每日一博 - Review线程池
Posted 小小工匠
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了每日一博 - Review线程池相关的知识,希望对你有一定的参考价值。
Pre
线程池(Thread Pool)是一种基于池化思想管理线程的工具.
使用线程池可以带来一系列好处:
- 降低资源消耗
- 提高响应速度
- 提高线程的可管理性
- …
线程池解决的核心问题就是资源管理问题。
在并发环境下,业务的不确定性,导致资源的依赖不确定。这种不确定性将带来以下若干问题:
- 频繁申请/销毁资源和调度资源,将带来额外的消耗,可能会非常巨大
- 对资源无限申请缺少抑制手段,易引发系统资源耗尽的风险
- 系统无法合理管理内部的资源分布,会降低系统的稳定性
为解决资源分配这个问题,线程池采用了“池化”(Pooling)思想。
核心设计与实现
在Java中的体现是ThreadPoolExecutor类 。 这里我们主要回顾 J.U.C提供的线程池 ThreadPoolExecutor
类 。
ThreadPoolExecutor实现的顶层接口是Executor 。
- 顶层接口Executor提供了一种思想:将任务提交和任务执行进行解耦。用户无需关注如何创建线程,如何调度线程来执行任务,用户只需提供Runnable对象,将任务的运行逻辑提交到执行器(Executor)中,由Executor框架完成线程的调配和任务的执行部分
- ExecutorService接口增加了一些能力:比如扩充执行任务的能力,补充可以为一个或一批异步任务生成Future的方法;提供了管控线程池的方法,比如停止线程池的运行等等。
- AbstractExecutorService则是上层的抽象类,将执行任务的流程串联了起来,保证下层的实现只需关注一个执行任务的方法即可
- 最下层的实现类ThreadPoolExecutor实现最复杂的运行部分,ThreadPoolExecutor将会一方面维护自身的生命周期,另一方面同时管理线程和任务,使两者良好的结合从而执行并行任务
运行机制
运行机制如下图所示
-
线程池在内部实际上构建了一个生产者消费者模型,将线程和任务两者解耦,并不直接关联,从而良好的缓冲任务,复用线程。
-
线程池的运行主要分成两部分:任务管理、线程管理。
任务管理部分充当生产者的角色,当任务提交后,线程池会判断该任务后续的流转:(1)直接申请线程执行该任务;(2)缓冲到队列中等待线程执行;(3)拒绝该任务。
线程管理部分是消费者,它们被统一维护在线程池内,根据任务请求进行线程的分配,当线程执行完任务后则会继续获取新的任务去执行,最终当线程获取不到任务的时候,线程就会被回收。
线程池的生命周期
线程池运行的状态,随着线程池的运行,由内部来维护。
线程池内部使用一个变量维护两个值:
- 运行状态(runState)
- 线程数量 (workerCount)
线程池将运行状态(runState)、线程数量 (workerCount)两个关键参数的维护通过ctl这个参数放在了一起
ctl 解读
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
AtomicInteger 类型, 该变量是对线程池的运行状态和线程池中有效线程的数量进行控制的一个字段, 它同时包含两部分的信息:线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount) .
高3位保存runState,低29位保存workerCount,两个变量之间互不干扰
用一个变量去存储两个值,可避免在做相关决策时,出现不一致的情况,不必为了维护两者的一致,而占用锁资源。
RUNNING = -1 << COUNT_BITS; //高3位为111
SHUTDOWN = 0 << COUNT_BITS; //高3位为000
STOP = 1 << COUNT_BITS; //高3位为001
TIDYING = 2 << COUNT_BITS; //高3位为010
TERMINATED = 3 << COUNT_BITS; //高3位为011
线程池也提供了若干方法去供用户获得线程池当前的运行状态、线程个数。这里都使用的是位运算的方式,相比于基本运算,速度也会快很多。
ctl的相关方法
// 获取运行状态;
private static int runStateOf(int c) { return c & ~CAPACITY; }
// 获取活动线程数;
private static int workerCountOf(int c) { return c & CAPACITY; }
// 获取运行状态和活动线程数的值。
private static int ctlOf(int rs, int wc) { return rs | wc; }
线程池的状态
线程池存在5种状态
1、RUNNING
(1) 状态说明:线程池处在RUNNING状态时,能够接收新任务,以及对已添加的任务进行处理。
(02) 状态切换:线程池的初始化状态是RUNNING。换句话说,线程池被一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0!
2、 SHUTDOWN
(1) 状态说明:线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务。
(2) 状态切换:调用线程池的shutdown()接口时,线程池由RUNNING -> SHUTDOWN。
3、STOP
(1) 状态说明:线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。
(2) 状态切换:调用线程池的shutdownNow()接口时,线程池由(RUNNING or SHUTDOWN ) -> STOP。
4、TIDYING
(1) 状态说明:当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。
(2) 状态切换:当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。 当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。
5、 TERMINATED
(1) 状态说明:线程池彻底终止,就变成TERMINATED状态。
(2) 状态切换:线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED。
进入TERMINATED的条件如下:
- 线程池不是RUNNING状态;
- 线程池状态不是TIDYING状态或TERMINATED状态;
- 如果线程池状态是SHUTDOWN并且workerQueue为空;
- workerCount为0;
- 设置TIDYING状态成功。
任务执行机制
任务调度
任务调度是线程池的主要入口,当用户提交了一个任务,接下来这个任务将如何执行都是由这个阶段决定的。这就是线程池的核心运行机制。
首先,所有任务的调度都是由execute方法完成的,这部分完成的工作是:检查现在线程池的运行状态、运行线程数、运行策略,决定接下来执行的流程,是直接申请线程执行,或是缓冲到队列中执行,亦或是直接拒绝该任务。
-
首先检测线程池运行状态,如果不是RUNNING,则直接拒绝,线程池要保证在RUNNING的状态下执行任务。
-
如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务。
-
如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。
-
如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务。
-
如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常。
流程图如下
任务缓冲
任务缓冲模块是线程池能够管理任务的核心部分。
线程池的本质是对任务和线程的管理,而做到这一点关键在于将任务和线程两者解耦,不让两者直接关联,才可以做后续的分配工作。
线程池中是以生产者消费者模式,通过一个阻塞队列来实现的。阻塞队列缓存任务,工作线程从阻塞队列中获取任务。
任务申请
任务拒绝
以上是关于每日一博 - Review线程池的主要内容,如果未能解决你的问题,请参考以下文章
Java Review - 线程池中使用ThreadLocal不当导致的内存泄漏案例&源码分析
Java Review - 线程池中使用ThreadLocal不当导致的内存泄漏案例&源码分析
Java Review - 并发编程_ CountDownLatch原理&源码剖析
Java Review - 并发编程_ CountDownLatch原理&源码剖析