图解 | 你管这破玩意叫线程池?

2023-11-05 18:50
文章标签 线程 图解 玩意 这破

本文主要是介绍图解 | 你管这破玩意叫线程池?,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

小宇:闪客,我最近看到线程池,被里边乱七八槽的参数给搞晕了,你能不能给我讲讲呀?

闪客:没问题,这个我擅长,咱们从一个最简单的情况开始,假设有一段代码,你希望异步执行它,是不是要写出这样的代码?

new Thread(r).start();

小宇:嗯嗯,最简单的写法似乎就是这样呢。

闪客:这种写法当然可以完成功能,可是你这样写,老王这样写,老张也这样写,程序中到处都是这样创建线程的方法,能不能写一个统一的工具类让大家调用呢?

小宇:可以的,感觉有一个统一的工具类,更优雅一些。

闪客:那如果让你来设计这个工具类,你会怎么写呢?我先给你定一个接口,你来实现。

public interface Executor {public void execute(Runnable r);
}

小宇:emmm,我可能先定义几个成员变量,比如核心线程数、最大线程数 ...反正就是那些乱七八糟的参数。

闪客:STOP!小宇呀,你现在深受面试手册的毒害,你先把这些全部的概念忘掉,就说让你写一个最简单的工具类,第一反应,你会怎么写?

第一版

小宇:那我可能会这样

// 新线程:直接创建一个新线程运行
class FlashExecutor implements Executor {public void execute(Runnable r) {new Thread(r).start();}
}

闪客:嗯嗯很好,你的思路非常棒。

小宇:啊,我这个会不会太 low 了呀,我还以为你会骂我呢。

怎么会, Doug Lea 大神在 JDK 源码注释中给出的就是这样的例子,这是最根本的功能。你在这个基础上,尝试着优化一下?

第二版

小宇:还能怎么优化呢?这不已经用一个工具类实现了异步执行了嘛!

闪客:我问你一个问题,假如有 10000 个人都调用这个工具类提交任务,那就会创建 10000 个线程来执行,这肯定不合适吧!能不能控制一下线程的数量呢?

小宇:这不难,我可以把这个任务 r 丢到一个 tasks 队列中,然后只启动一个线程,就叫它 Worker 线程吧,不断从 tasks 队列中取任务,执行任务。这样无论调用者调用多少次,永远就只有一个 Worker 线程在运行,像这样。

闪客:太棒了,这个设计有了三个重大的意义:

1. 控制了线程数量。

2. 队列不但起到了缓冲的作用,还将任务的提交与执行解耦了。

3. 最重要的一点是,解决了每次重复创建和销毁线程带来的系统开销。

小宇:哇真的么,这么小的改动有这么多意义呀。

闪客:那当然,不过只有一个后台的工作线程 Worker 会不会少了点?还有如果这个 tasks 队列满了怎么办呢?

第三版

小宇:哦,的确,只有一个线程在某些场景下是很吃力的,那我把 Worker 线程的数量增加?

闪客:没错,Worker 线程的数量要增加,但是具体数量要让使用者决定,调用时传入,就叫核心线程数 corePoolSize 吧。

小宇:好的,那我这样设计。

1. 初始化线程池时,直接启动 corePoolSize 个工作线程 Worker 先跑着。

2. 这些 Worker 就是死循环从队列里取任务然后执行。

3. execute 方法仍然是直接把任务放到队列,但队列满了之后直接抛弃

闪客:太完美了,奖励你一块费列罗吧。

小宇:哈哈谢谢,那我先吃一会儿哈。 

闪客:好,你边吃我边说。现在我们已经实现了一个至少不那么丑陋的线程池了,但还有几处小瑕疵,比如初始化的时候,就创建了一堆 Worker 线程在那空跑着,假如此时并没有异步任务提交过来执行,这就有点浪费了。

小宇:哦好像是诶!

闪客:还有,你这队列一满,就直接把新任务丢弃了,这样有些粗暴,能不能让调用者自己决定该怎么处理呢?

小宇:哎呀,想不到我这么温柔的妹纸居然写出了这么粗暴的代码。

闪客:额,你先把费列罗咽下去吧。

第四版

小宇:我吃完了,现在脑子有点不够用了,得先消化消化食物,要不你帮我分析分析吧。

闪客:好的,现在我们做出如下改进。

1. 按需创建Worker:刚初始化线程池时,不再立刻创建 corePoolSize 个工作线程,而是等待调用者不断提交任务的过程中,逐渐把工作线程 Worker 创建出来,等数量达到 corePoolSize 时就停止,把任务直接丢到队列里。那就必然要用一个属性记录已经创建出来的工作线程数量,就叫 workCount 吧。

2. 加拒绝策略:实现上就是增加一个入参,类型是一个接口 RejectedExecutionHandler,由调用者决定实现类,以便在任务提交失败后执行 rejectedExecution 方法。

3. 增加线程工厂:实现上就是增加一个入参,类型是一个接口 ThreadFactory,增加工作线程时不再直接 new 线程,而是调用这个由调用者传入的 ThreadFactory 实现类的 newThread 方法。

就像下面这样。

小宇:哇,还是你厉害,这一版应该很完美了吧?

闪客:不不不,离完美还差得很远,接下来的改进,由你来想吧,我这里可以给你一个提示

弹性思维

第五版

小宇:弹性思维?哈哈闪客你这术语说的真是越来越不像人话了

闪客:咳咳

小宇:哦,我是说你肯定是指我这个代码写的没有弹性,对吧?可是弹性是指什么呢?

闪客:简单说,在这个场景里,弹性就是在任务提交比较频繁,和任务提交非常不频繁这两种情况下,你这个代码是否有问题?

小宇:emmm 让我想想,我这个线程池,当提交任务的量突增时,工作线程和队列都被占满了,就只能走拒绝策略,其实就是被丢弃掉

闪客:是的

小宇:这样的确是太硬了,诶不过我想了下,调用方可以通过设置很大的核心线程数 corePoolSize 来解决这个问题呀。

闪客:的确是可以,但一般场景下 QPS 高峰期都很短,而为了这个很短暂的高峰,设置很大的核心线程数,简直太浪费资源了,你看上面的图不觉得眼晕么?

小宇:是呀,那怎么办呢,太大了也不行,太小了也不行。

闪客:我们可以发明一个新的属性,叫最大线程数 maximumPoolSize。当核心线程数和队列都满了时,新提交的任务仍然可以通过创建新的工作线程(叫它非核心线程),直到工作线程数达到 maximumPoolSize 为止,这样就可以缓解一时的高峰期了,而用户也不用设置过大的核心线程数。

小宇:哦好像有点感觉了,可是具体怎么操作呢?

闪客:想象力不行呀小宇,那你看下面的演示。

1. 开始的时候和上一版一样,当 workCount < corePoolSize 时,通过创建新的 Worker 来执行任务。

2. 当 workCount >= corePoolSize 就停止创建新线程,把任务直接丢到队列里。

3. 但当队列已满且仍然 workCount < maximumPoolSize 时,不再直接走拒绝策略,而是创建非核心线程,直到 workCount = maximumPoolSize,再走拒绝策略。

小宇:哎呀,我怎么没想到,这样 corePoolSize 就负责平时大多数情况所需要的工作线程数,而 maximumPoolSize 就负责在高峰期临时扩充工作线程数。

闪客:没错,高峰时期的弹性搞定了,那自然就还要考虑低谷时期。当长时间没有任务提交时,核心线程与非核心线程都一直空跑着,浪费资源。我们可以给非核心线程设定一个超时时间 keepAliveTime,当这么长时间没能从队列里获取任务时,就不再等了,销毁线程。

小宇:嗯,这回咱们的线程池在 QPS 高峰时可以临时扩容,QPS 低谷时又可以及时回收线程(非核心线程)而不至于浪费资源,真的显得十分 Q 弹呢。

闪客:是呀是呀。诶不对,怎么又变成我说了,不是说这一版你来思考么?

小宇:我也想啊,但你这一讲技术就自说自话的毛病老是不改,我有啥办法。

闪客:额抱歉抱歉,那接下来你总结一下我们的线程池吧

总结

小宇:嗯好的,首先它的构造方法是这个样子滴

public FlashExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler) 
{... // 省略一些参数校验this.corePoolSize = corePoolSize;this.maximumPoolSize = maximumPoolSize;this.workQueue = workQueue;this.keepAliveTime = unit.toNanos(keepAliveTime);this.threadFactory = threadFactory;this.handler = handler;
}

这些参数分别是

int corePoolSize:核心线程数

int maximumPoolSize:最大线程数

long keepAliveTime:非核心线程的空闲时间

TimeUnit unit:空闲时间的单位

BlockingQueue<Runnable> workQueue:任务队列(线程安全的阻塞队列)

ThreadFactory threadFactory:线程工厂

RejectedExecutionHandler handler:拒绝策略

整个任务的提交流程是

闪客:不错不错,这可是你自己总结的哟,现在还用我给你讲什么是线程池了么?

小宇:啊天呢,我才发现这似乎就是我一直弄不清楚的线程池的参数和原理呢!

闪客:没错,而且最后一版代码的构造方法,就是 Java 面试常考的 ThreadPoolExecutor 最长的那个构造方法,参数名都没变。

小宇:哇,太赞了!我都忘了一开始我想干嘛了,嘻嘻。

闪客:哈哈,不知不觉学到了技术才爽呢,对吧?晚饭时间快到了,要不要一块去吃山西面馆呀?

小宇:哦,那家店餐桌的颜色我不太喜欢,下次吧。

闪客:哦好吧。

后记 

线程池是面试常考的知识点,网上很多文章都是直接从它那有 7 个参数的构造方法讲起,强行把各个参数的含义说给你听,让人云里雾里。

希望读者读完本篇文章后,线程池的这些参数不再是死记硬背,而是像本文中这些动图一样在你的脑中活灵活现,这样就能永远记住他们啦~

本文中各个版本都有对应代码,在公众号 低并发编程 后台回复 线程池 即可获取。

低并发编程,周一很颓废,周四很硬核

这篇关于图解 | 你管这破玩意叫线程池?的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/351813

相关文章

线程的四种操作

所属专栏:Java学习        1. 线程的开启 start和run的区别: run:描述了线程要执行的任务,也可以称为线程的入口 start:调用系统函数,真正的在系统内核中创建线程(创建PCB,加入到链表中),此处的start会根据不同的系统,分别调用不同的api,创建好之后的线程,再单独去执行run(所以说,start的本质是调用系统api,系统的api

图解TCP三次握手|深度解析|为什么是三次

写在前面 这篇文章我们来讲解析 TCP三次握手。 TCP 报文段 传输控制块TCB:存储了每一个连接中的一些重要信息。比如TCP连接表,指向发送和接收缓冲的指针,指向重传队列的指针,当前的发送和接收序列等等。 我们再来看一下TCP报文段的组成结构 TCP 三次握手 过程 假设有一台客户端,B有一台服务器。最初两端的TCP进程都是处于CLOSED关闭状态,客户端A打开链接,服务器端

java线程深度解析(六)——线程池技术

http://blog.csdn.net/Daybreak1209/article/details/51382604 一种最为简单的线程创建和回收的方法: [html]  view plain copy new Thread(new Runnable(){                @Override               public voi

java线程深度解析(五)——并发模型(生产者-消费者)

http://blog.csdn.net/Daybreak1209/article/details/51378055 三、生产者-消费者模式     在经典的多线程模式中,生产者-消费者为多线程间协作提供了良好的解决方案。基本原理是两类线程,即若干个生产者和若干个消费者,生产者负责提交用户请求任务(到内存缓冲区),消费者线程负责处理任务(从内存缓冲区中取任务进行处理),两类线程之

java线程深度解析(四)——并发模型(Master-Worker)

http://blog.csdn.net/daybreak1209/article/details/51372929 二、Master-worker ——分而治之      Master-worker常用的并行模式之一,核心思想是由两个进程协作工作,master负责接收和分配任务,worker负责处理任务,并把处理结果返回给Master进程,由Master进行汇总,返回给客

java线程深度解析(二)——线程互斥技术与线程间通信

http://blog.csdn.net/daybreak1209/article/details/51307679      在java多线程——线程同步问题中,对于多线程下程序启动时出现的线程安全问题的背景和初步解决方案已经有了详细的介绍。本文将再度深入解析对线程代码块和方法的同步控制和多线程间通信的实例。 一、再现多线程下安全问题 先看开启两条线程,分别按序打印字符串的

java线程深度解析(一)——java new 接口?匿名内部类给你答案

http://blog.csdn.net/daybreak1209/article/details/51305477 一、内部类 1、内部类初识 一般,一个类里主要包含类的方法和属性,但在Java中还提出在类中继续定义类(内部类)的概念。 内部类的定义:类的内部定义类 先来看一个实例 [html]  view plain copy pu

图解可观测Metrics, tracing, and logging

最近在看Gophercon大会PPT的时候无意中看到了关于Metrics,Tracing和Logging相关的一篇文章,凑巧这些我基本都接触过,也是去年后半年到现在一直在做和研究的东西。从去年的关于Metrics的goappmonitor,到今年在排查问题时脑洞的基于log全链路(Tracing)追踪系统的设计,正好是对这三个话题的实践。这不禁让我对它们的关系进行思考:Metrics和Loggi

C#线程系列(1):BeginInvoke和EndInvoke方法

一、线程概述 在操作系统中一个进程至少要包含一个线程,然后,在某些时候需要在同一个进程中同时执行多项任务,或是为了提供程序的性能,将要执行的任务分解成多个子任务执行。这就需要在同一个进程中开启多个线程。我们使用 C# 编写一个应用程序(控制台或桌面程序都可以),然后运行这个程序,并打开 windows 任务管理器,这时我们就会看到这个应用程序中所含有的线程数,如下图所示。

71-java 导致线程上下文切换的原因

Java中导致线程上下文切换的原因通常包括: 线程时间片用完:当前线程的时间片用完,操作系统将其暂停,并切换到另一个线程。 线程被优先级更高的线程抢占:操作系统根据线程优先级决定运行哪个线程。 线程进入等待状态:如线程执行了sleep(),wait(),join()等操作,使线程进入等待状态或阻塞状态,释放CPU。 线程占用CPU时间过长:如果线程执行了大量的I/O操作,而不是CPU计算