多线程 Part 5 - 线程池
- 一、两种线程模型
- 1. 用户线程ULT
- 2. 内核线程KLT
- 二、为什么要有线程池
- 三、线程池的好处
- 四、线程池使用
- 1. 三大使用
- 2. 七大参数
- 3. 线程池结构
- 4. 四种拒绝策略
- 5. 怎么确定最大核心线程数
- 五、线程池的五种状态
- 六、线程池工作原理
- execute()
- addWorker() 上半部分
- addWorker() 下半部分
- runWorker()
- getTask()
- processWorkerExit()
- tryTerminate()
- shutdown()
- shutdownNow()
- 七、总结
由用户程序实现,不依赖于操作系统。不需要用户态与核心态的切换,速度快。内核对用户线程无感知,线程阻塞则进程阻塞。
由操作系统管理,线程阻塞不会引起进程阻塞。在多处理器下,多线程在多处理器上并行运行。效率比用户线程低。JVM基本用的是KLT。从Java创建的线程会1:1的对应到内核,然后由CPU调度执行。
创建线程和销毁线程都是很耗资源的操作,而Java线程依赖内核线程,创建线程需要进行操作系统状态切换。为了避免过度消耗资源,有了线程池,作为线程缓存的区域。
- 降低资源消耗
- 提高响应速度
- 方便管理
- 线程复用,可以控制最大并发数,管理线程
a). 单个线程
输出:
b). 创建一个固定大小的线程池,这里大小设为3,所以最多会有3个线程执行
输出:
c). 能有多少就有多少,这里输出创建了8个线程
输出:
无论是single, fixed, Cached,通通调用的都是 ThreadPoolExecutor 类来进行线程池创建
我们来看一下 ThreadPoolExecutor 类构造器,参数列表里就是传说中的七大参数
在阿里巴巴开发手册里面规定了,不要用Executors创建线程池,而是要用ThreadPoolExecutor来创建,因为确实用Executors不安全,有OOM问题。
那么我们先来举个例子看一下线程池的结构,再来讨论着七大参数分别有什么意义。




我们一个个来研究一下都是什么策略:
- AbortPolicy:直接拒绝,可以看到方法里面直接抛出了一个异常
- CallerRunsPolicy:让线程执行它原有的业务,可谓从哪里来回哪里去。线程池不会受理它的业务。如果线程池正好这个时候被关闭了,那进来的这个线程就回不到原来的地方了。
- DiscardPolicy:可以看到里什么也没做,所以进来的线程也是直接被拒绝,只不过不会报异常
- DiscardOldestPolicy:这个和前三个有很大的不同。如果线程池关闭了,那当然什么也不会发生。如果线程池开着,那就把阻塞队列里面第一个排队的给移走,然后尝试让线程池受理新进来的这个线程。
- CPU密集型:把 maximumPoolSize 就设置为CPU核数
- IO密集型:判断程序中十分占IO的任务有多少个,然后设置 maximumPoolSize 至少大于这个个数
- Running:能接受新任务以及处理已添加的任务
- Shutdown:不接受新任务,但可以处理已经添加的任务
- Stop:不接受新任务,不处理已经添加的任务,并且中断正在处理的任务
- Tidying:所有的任务已经终止
- Terminated:线程池彻底终止
线程池状态转换

我们发现五种状态被存在一个32位的Integer里。这里高3位代表状态,低29位代表当前工作的线程数
- 如果当前工作线程小于核心线程数,那就创建一个核心线程worker并且负责执行传入的command任务
- 如果当前工作线程大于核心线程数,那就把传入的command任务放入阻塞队列里
- 如果阻塞队列满了,传入的command无法放入,那就尝试增加非核心线程worker
- 如果非核心线程数也满了,那就使用拒绝策略,拒绝传入的command任务
- 以上情况都是建立在线程池running状态,如果线程池准备终止了,那传入的command不会被受理,也不会有新的worker被创建
通过内外两层循环,先检查线程池当前可不可以增加新的worker,不可以增加新worker的情况有两大类:
- 线程池不在running状态
a). 线程池处于Stop, Tidying, Terminated
b). 线程池接受了新任务
c). 阻塞队列为空 - 线程池在running状态
a). 当前工作线程数已经达到了理论最大值0x1fffffff
b). 当前工作线程数超过了核心线程数或者最大线程数(取决于参数core的值)
简单的说,如果线程池不在running状态了,那线程池要想创建新worker,必须是在线程池shutdown状态,因为这个时候线程池虽然不接受新任务,但之前已接受的任务还都会处理完。而且这个时候,创建的worker只会是非核心线程,而且只会从阻塞队列里去拿任务。如果线程池还在running状态,那线程池数量必须在要求的限制范围内。
检查后,确定可以创建新worker,那就通过CAS将工作线程数加1,真正创建新worker要到的下半部分
实质的创建了新worker,并且启动线程,让这个worker开始工作。当前线程启动的条件是线程池还处于running状态,或者处于shutdown状态而且没有接受新任务。最后返回布尔值代表线程是否成功启动。
看看Worker构造器,将传入的firstTask赋给Worker对象的Runnable变量,再将Worker对象的线程初始化,但注意,这时还没有将firstTask交给线程处理
Worker类继承了Runnable接口,真正启动线程调用的就是Worker类里的run方法
如果worker的task是空的话,那就从阻塞队列里面去取task,并且这里是个while大循环,不断地去判断是不是有事务要做,实现了线程池的复用机制。如果在阻塞队列取task超时了,那就会跳出while往finally走,准备回收当前这个worker。
在阻塞队列取task的工作在里完成;回收worker的工作在里完成
从阻塞队列里去取task。有3种情况会决定要回收当前worker:
- 线程池处于shutdown,而且阻塞队列为空。不会接受新任务了,也没有旧任务做了,回收
- 线程池处于stop。新旧任务都不会做了,回收
- 如果当前线程数超过了核心线程数(默认核心线程不要求回收),并且从阻塞队列取task超时,回收
注意,如果核心线程也要求回收(取决于allowCoreThreadTimeOut),那核心线程超时了也一样会被回收,哪怕最后所有线程全都回收了。
如果这里决定要回收worker了,会预先把工作线程数减1,然后由真正执行回收工作。但如果由于中断等原因,这个worker应该要回收了,但工作线程数没有减1,那工作线程数减1的工作也会在里补充完成。
如果线程是非正常结束的,那就会先将工作线程数减1。然后从workers哈希表中移除当前这个worker。再尝试终止线程池。如果当前线程池处在shutdown或者running状态,那有两种情况可能会需要再次补充创建非核心线程:
- 当前worker是非正常结束的
- 当前worker是正常结束的,但当前线程池工作线程数量小于最少需求数
线程池可以结束的条件:
- 线程池不在running状态
- 线程池不在shutdown状态;或者线程池在shutdown状态,但阻塞队列为空
线程池具备可以结束的条件之后,一次会尝试中断一个闲置线程(如果线程正在工作那就不会被中断),然后被中断的线程就会被回收,再去中断另一个线程,直到所有线程都被回收,线程池开始终止。
将线程池转为shutdown状态,然后结束线程池
同样也是结束线程池,但和不一样的是:
- 有返回值,返回的就是阻塞队列里没完成的任务
- 线程池状态先转为的是Stop
- 会中断所有的线程,包括正在工作的
线程池原理简单的讲就和银行办理业务的流程很相似,一开始只有核心线程受理业务,核心线程全部开启了,任务就会进阻塞队列排队,阻塞队列排满了,就会开启非核心线程,非核心线程也满了,就会启动拒绝策略。
每个工作线程处理好手头的业务之后,都会循环的去阻塞队列里取新任务来执行,实现线程的复用。
任务减少的时候,非核心线程如果长时间没有拿到任务就会被回收。但是核心线程如果不要求回收,即使没有任务,它们也会一直待在线程池里。
需要注意的是,在线程池内部,其实并没有对每个线程进行核心/非核心线程的标记。核心/非核心线程仅仅是在数量上进行了定义,而这个数量的限制规定了我们什么时候要创建新的工作线程,什么时候回收旧的线程,以及线程池里应该留下多少线程。
到此这篇java阻塞队列实现原理(java阻塞队列线程安全吗)的文章就介绍到这了,更多相关内容请继续浏览下面的相关推荐文章,希望大家都能在编程的领域有一番成就!版权声明:
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若内容造成侵权、违法违规、事实不符,请将相关资料发送至xkadmin@xkablog.com进行投诉反馈,一经查实,立即处理!
转载请注明出处,原文链接:https://www.xkablog.com/jjc/12313.html