文章目录
一、线程池的好处
线程使应用能够更加充分合理地协调利用CPU、内存、网络、I/O等系统资源。线程的创建需要开辟虚拟机栈、本机方法栈、程序计数器等线程私有的内存空间。在线程销毁时需要回收这些资源。 频繁地创建和销毁线程会浪费大量的系统资源,增加并发编程风险。另外,在服务器负载过大的时候,如何让新的线程等待或者友好地拒绝服务?这些都是线程自身无法解决的。 所以需要通过线程池协调多个线程,并实现类似主次线程隔离、定时执行、周期执行等任务。
1.1 线程池的作用:
- 利用线程池管理并复用线程、控制最大并发数等。
- 实现任务线程队列缓存策略和拒绝机制
- 实现某些与时间相关的功能,如定时执行、周期执行等
- 隔离线程环境。比如,交易服务和搜索服务在同一台服务器上,分别开启两个线程池,交易线程的资源消耗明显要大;因此,通过配置独立的线程池,将较慢的交易与搜索服务隔离开,避免各服务线程相互影响。
1.2 线程是如何创建的
首先从 ThreadPoolExecutor 构造方法分析,如何自定义ThreadFactory 和 RejectedExecutionHandler ,通过分析ThreadExecutor 的execute 和addWorker 两个核心方法,学习如何把任务线程加入到线程池中运行。
ThreadPoolExecutor 的构造方法如下:
public ThreadPoolExecutor(
int corePoolSize, // (第1个参数)
int maximumPoolSize, // (第2个参数)
long keepAliveTime, // (第3个参数)
TimeUnit unit, // (第4个参数)
BlockingQueue<runnable> workQueue, // (第5个参数)
ThreadFactory threadFactory, // (第6个参数)
RejectedExecutionHandler handler) { // (第7个参数)
if(corePoolSize < 0 ||
// maximumPoolSize 必须大于或等于1 也要大于或等于 corePoolSize (第1处)
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
(第2处)
if (workQueue == null || ThreadFactory == null || handler == null)
throw new NullPointerException();
// 其他代码 ...
}
- 第1个参数: corePoolSize
表示常驻核心线程池数。如果等于0,则任务执行完之后,没有任何请求进入时销毁线程池的线程;如果大于 0, 即使本地任务执行完毕,核心线程也不会被销毁。这个值的设置非常关键,设置过大会浪费资源,设置过小会导致线程频繁地创建或销毁。 - 第2个参数:maximumPoolSize
表示线程池能够容纳同时执行的最大线程数。从上方代码中第1处来看,必须大于或等于1。如果maximumPoolSize 与 corePoolSize 相等,即是固定大小线程池。 - 第3个参数:keepAliveTime
表示线程池中的线程空闲时间。当空闲时间达到keepAliveTime 值时,线程会被销毁,直到只剩下 corePoolSize 个线程为止,避免浪费内存和句柄资源。在默认情况下,当线程池的线程数大于 corePoolSize 时,keepAliveTime 才会起作用。但是当ThreadPoolExecutor 的allowCoreThreadTimeOut 变量设置为true 时,核心线程超时后也被回收。 - 第4个参数:TimeUnit
表示时间单位。keepAliveTime 的时间单位通常是TimeUnit.SECONDS 。 - 第5个参数:workQueue
表示缓存队列。当请求的线程数大于corePoolSize 时,线程进入BlockingQueue 阻塞队列,BlockingQueue 队列缓存达到上限后,如果还有新任务需要处理,那么线程池会创建新的线程,最大线程数为 maximumPoolSize 。例如一个生产消费模型队列,使用LinkedBlockingQueue 是单向链表,使用锁来控制入队和出队的原子性,两个锁分别控制元素的添加和获取。 - 第6个参数: threadFactory
表示线程工厂。它用来生成一组相同任务的线程。线程池的命令是通过给这个factory 增加组名前缀来实现的。在虚拟机栈分析时,就可以知道线程任务是由哪个线程工厂产生的。 - 第7个参数: handler
表示执行拒绝策略的对象。当第5个参数workQueue 的任务缓存区到达上限后,并且活动线程数大于maximumPoolSize 的时候,线程池通过该策略处理请求,这是一种简单的限流保护。像某年双十一没有处理好访问流量过载时的拒绝策略,导致内部测试页面被展示出来,使用户手足无措。友好的拒绝策略可以是如下三种:
(1)保存到数据库进行削峰填谷。在空闲时在提取出来执行。
(2)转向某个提示页面。
(3)打印日志。
从第2处来看,队列、线程工厂、拒绝处理服务都必须有实例对象,但在实际编程中,很少有程序员对这三者进行实例化,而通过Executor 这个线程池静态工厂提供默认实现,那么Executor 与 ThreadPoolExecutor 是什么关系呢?线程池相关类图如下图所示:
/**
* @param 线程任务
* @throws RejectedExecutionException 如果无法创建任何状态的线程任务
*/
void execute (Runnable command);
ExecutorService 接口继承了Executor 接口,定义了管理线程任务的方法。ExecutorService 的抽象类 AbstractExecutorService 提供了submit()、invokeAll() 等部分方法的实现,但是核心方法Executor.execute() 并没有在这里实现。因为所有的任务都在这个方法里执行,不同实现会带来不同的执行策略,这一点在后续的ThreadPoolExecutor 解析时,会进一步分析。通过Executors 的静态工厂方法可以创建三个线程池的包装对象:ForkJoinPool、ThreadPoolExecutor、ScheduledThreadPoolExecutor。
1.3 Executors 分析
Executors 核心的方法由五个:
- Executors.newWorkStealingPool:
JDK8 引入,创建持有足够线程的线程池支持给定的并行度,并通过使用多个队列减少竞争,此构造方法中把CPU 数量设置为默认的并行度:
public statice ExecutorService newWorkStealingPool(){
// 返回 ForkJoinPool (JDK7引入)对象,它也是AbstractExecutorService 的子类
return new ForkJoinPool (Runtime.getRuntime().availableProcessors(),
ForkJoinPool.defaultForkJoinWorkerThreadFactory, null, true);
}
-
Executors.newCachedThreadPool :
maximumPoolSize 最大可以至Integer.MAX_VALUE , 是高度可伸缩的线程池,如果达到这个上限,相信没有服务器能够继续工作,肯定会抛出OOM 异常。keepAliveTime 默认为60 秒,工作线程处于空闲状态,则回收工作线程。如果认为数增加,再次创建出新线程处理任务。 -
Executors.newScheduledThreadPool:
线程数最大至Integer.MAX_VALUE ,与上述相同,存在OOM 风险。它是ScheduledExecutorService 接口家族的实现类,支持定时及周期性任务执行。相比Timer,ScheduledExecutorService 更安全,功能更强大,与newCachedThreadPool 的区别是不回收工作线程。 -
Executors.newSingleThreadExecutor:
创建一个单线程的线程池,相当于单线程串行执行所有任务,保证按任务的提交顺序依次执行。 -
Executors.newFixedThreadPool :
输入的参数即是固定线程数,既是核心线程数也是最大线程数,不存在空闲线程,所以keepAliveTime 等于 0 :
public static ExecutorService newFixedThreadPool(int nThreads){
return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlokingQueue<runnable>());
}
这里,输入的队列没有指明长度,下面为LinkedBlockingQueue 的构造方法:
public LinkedBlockingQueue () {
this(Integer.MAX_VALUE);
}
使用这样的无界队列,如果瞬间请求非常大,会有OOM 的风险。除newWorkStealingPool 外,其他四个创建方式都存在资源耗尽的风险。
Executors 中默认的线程工厂和拒绝策略过于简单,通常对用户不够友好。线程工厂需要做创建前的准备工作,对线程池创建的线程必须明确标识,就像药品的生成批号一样,为线程本身指定有意义的名称和相应的序列号。拒绝策略应该考虑到业务场景,返回相应的提示或友好地跳转。
1.3.1 ThreadFactory 示例
public class UserThreadFactory implements ThreadFactory {
private final String namePrefix;
private final AtomicInteger nextId = new AtomicInteger(1);
// 定义线程组名称,在使用jstack 来排查线程问题时,非常有帮助
UserThreadFactory(String whatFeatureOfGroup){
namePrefix = "UserThreadFactory's "+ whatFeatureOfGroup + "-Worker";
}
@Override
public Thread newThread(Runnable task) {
String name = namePrefix + nextId.getAndIncrement();
Thread thread = new Thread(null,task,name,0);
System.out.println(thread.getName());
return thread;
}
public static void main(String[] args) {
UserThreadFactory factory = new UserThreadFactory("abide");
Task task = new Task();
factory.newThread(task).start();
}
}
// 任务执行体
class Task implements Runnable{
private final AtomicLong count = new AtomicLong(0L);
@Override
public void run() {
System.out.println("running_"+count.getAndIncrement());
}
}
上述示例包括线程工厂和任务执行体的定义,通过newThread 方法快速、统一地创建线程任务,强调线程一定要有特定意义的名称,方便出错时回溯。
1.3.2 RejectedExecutionHandler 的简单实现
下面代码实现了RejectedExecutionHandler ,实现了接口的rejectedExecution 方法,打印出当前线程池状态,源码如下:
public class UserRejectHandler implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable task, ThreadPoolExecutor executor){
System.out.println("task rejected. " + executor.toString());
}
}
在ThreadPoolExecutor 中提供了四个公开的内部静态类:
- AbortPolicy(默认):丢弃任务并抛出RejectedExecutionException 异常。
- DiscardPolicy:丢弃任务,但是不抛弃异常,这是不推荐的做法。
- DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中。
- CallerRunsPolicy: 调用任务的run() 方法绕过线程池直接执行。
根据之前实现的线程工厂和拒绝策略,线程池的相关代码实现如下:
public class UserThreadPool {
public static void main(String[] args) {
// 缓存队列设置固定长度为2,为了快速触发 rejectHandler
BlockingQueue queue = new LinkedBlockingQueue(2);
// 假设外部任务线程的来源由机房1 和机房2 的混合调用
UserThreadFactory f1 = new UserThreadFactory("第1机房");
UserThreadFactory f2 = new UserThreadFactory("第2机房");
UserRejectHandler handler = new UserRejectHandler();
// 核心线程为1, 最大线程为2,为了保证触发rejectHandler
ThreadPoolExecutor threadPoolFirst =
new ThreadPoolExecutor(1,2,60, TimeUnit.SECONDS, queue, f1, handler);
// 利用第二个线程工厂实例创建第二个线程池
ThreadPoolExecutor threadPoolSecond =
new ThreadPoolExecutor(1, 2, 60, TimeUnit.SECONDS, queue ,f2, handler);
// 创建400个任务线程
Runnable task = new Task();
for (int i = 0; i < 200; i++) {
threadPoolFirst.execute(task);
threadPoolSecond.execute(task);
}
}
}
执行结果如下:
UserThreadFactory’s 第1机房-Worker1
UserThreadFactory’s 第2机房-Worker1
UserThreadFactory’s 第1机房-Worker2
running_0
UserThreadFactory’s 第2机房-Worker2
running_1
running_2
running_3
running_4
running_6
running_5
running_7
running_8
…
task rejected. java.util.concurrent.ThreadPoolExecutor@3f99bd52[Running, pool size = 2, active threads = 2, queued tasks = 2, completed tasks = 120]
当任务被拒绝的时候,拒绝策略会打印出当前线程池的大小已经达到了maximumPoolSize=2,且队列已满,完成的任务数提示已经有120个(最后一行)。
二、线程池源码详解
2.1 ThreadPoolExecutor 的源码分析
在ThreadPoolExecutor 的属性定义中频繁地用位移运算来表示线程池状态,位移运算时改变当前值的一种高效手段,包括左移与右移。下面从属性定义开始分析ThreadPoolExecutor 的源码:
// Integer 共有32位,最右边29位表示工作线程数,最左边3位表示线程池状态
// 注:简单地说,3个二进制可以表示从0至7的8个不同的数值 (第1处)
private static final int COUNT_BITS = Integer.SIZE - 3;
// 000-11111111111111111111111111111,类似于子网掩码,用于为的与运算,得到左边3位,还是右边29位
private static final int COUNT_MASK = (1 << COUNT_BITS) - 1;
// 用左边3位,实现5种线程池状态。(在左3位之后加入中画线有助于理解)
// 111-00000000000000000000000000000,十进制值:-536,870,912
// 此状态表示线程池能接受新任务
private static final int RUNNING = -1 << COUNT_BITS;
// 000-00000000000000000000000000000,十进制:0
// 此状态不再接受新任务,但可以继续执行行队列中的任务
private static final int SHUTDOWN = 0 << COUNT_BITS;
// 001-00000000000000000000000000000,十进制:536,870,912
// 此状态全面拒绝,并中断正在处理的任务
private static final int STOP= 1 << COUNT_BITS;
// 010-00000000000000000000000000000,十进制:1,073,741,824
// 此状态表示所有任务已经被终止
private static final int TIDYING = 2 << COUNT_BITS;
// 011-00000000000000000000000000000,十进制:1,610,612,736
// 此状态表示已清理完现场
private static final int TERMINATED= 3 << COUNT_BITS;
// 与运算,比如 001-00000000000000000000000100011,表示67个工作线程,
// 掩码取反: 111-00000000000000000000000000000,即得到左边3位001,表示线程池当前处于STOP 状态
private static int runStateOf(int c) { return c & ~COUNT_MASK; }
// 同理掩码 000-11111111111111111111111111111,得到右边29位,即工作线程数
private static int workerCountOf(int c) { return c & COUNT_MASK; }
// 把左边3位与右边29位按或运算,合并成一个值
private static int ctlOf(int rs, int wc) {return rs | wc; }
第一处说明,线程池的状态用高3位表示,其中包括了符号位。五种状态的十进制按从小到大依次排序为:RUNNING < SHUTDOWN < STOP < TIDYING < TERMINATED ,这样设计的好处是可以通过比较值的大小来确定线程池的状态。
例如程序中经常会出现isRunning 的判断:
private static boolean isRunning(int c) {
return c < SHUTDOWN;
}
2.2 ThreadPoolExecutor 关于execute 方法的分析
我们知道Executor 接口有且只有一个方法execute ,通过参数传入待执行线程的对象。下面分析ThreadPoolExecutor 关于execute 方法的实现:
public void execute (Runnable comand){
// 返回包含线程数及线程池状态的Integer 类型数值
int c = ctl.get();
// 如果工作线程数小于核心线程数,则创建线程任务并执行
if (workerCountOf(c) < corePoolSize){
// addWorker 是另一个极为重要的方法, (第1处)
if(addWorker(command, true))
return;
// 如果创建失败,防止外部已经在线程池中加入新任务,重新获取一下
c = ctl.get();
}
// 只有线程池处于RUNNING 状态,才执行后半句: 置入队列
if(isRunning(c) && WorkQueue.offer(command)){
int recheck = ctl.get();
// 如果线程池不是RUNNING 状态,则将刚加入队列的任务移出
if(! isRunning(recheck) && remove(command))
reject(command);
// 如果之前的线程已被消费完,新建一个线程
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
// 核心池和队列都已满,尝试创建一个新线程
} else if (!addWorker(command, fasle))
// 如果addWorker 返回时false,即创建失败,则唤醒拒绝策略 (第2处)
reject(command);
}
第1处: execute 方法在不同的阶段有三次addWorker 的尝试动作。
第2处: 发生拒绝的理由有两个:
(1) 线程池状态为非RUNNING 状态
(2) 等待队列已满
2.2.1 addWorker 方法源码分析
下面是ThreadPoolExecutor 类中addWorker 方法源码分析
/**
* 根据当前线程池状态,检查是否可以添加新的任务线程,如果key则创建并启动任务
* 如果一切正常则返回true。返回false 的可能性如下:
* 1.线程池没有处于RUNNING 状态
* 2.线程工厂创建新的任务线程失败
*
* firstTask:外部启动线程池时需要构造的第一个线程,它是线程的母体
* core: 新增工作线程时的判断指标,解释如下:
* true 表示新增工作线程时,需要判断当前RUNNING 状态的线程是否少于 corePoolSize
* false 表示新增工作线程时,需要判断当前RUNNING 状态的线程是否少于 maximumPoolSize
*/
private boolean addWorker(Runnable firstTask, boolean core) {
// 不需要任务预定义的语法标签,响应下文的continue retry,快速退出多层嵌套循环 (第1处)
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// 参考之前的状态分析:如果是RUNNING 状态,则条件为假,不执行后面的判断
// 如果状态不为SHUTDOWN ,或firstTask 初始线程不为空,或者队列为空,都会直接返回创建失败 (第2处)
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
int wc = workerCountOf(c);
// 如果超过最大允许线程数则不能再添加新的线程
// 最大线程数不能超过2^29 ,否则影响左边3位的线程池状态值
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
// 将当前活动线程数 +1 (第3处)
if (compareAndIncrementWorkerCount(c))
break retry;
// 线程池和工作线程数是可变化的,需要经常提取这个最新值
c = ctl.get(); // Re-read ctl
// 如果已经关闭,则再次从retry 标签处进入,在第2处再做判断 (第4处)
if (runStateOf(c) != rs)
continue retry;
// 如果线程还是处于RUNNING 状态,那就再说明仅仅是第3处失败 ,继续循环执行 (第5处)
}
}
// 开始创建工作线程
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
// 利用Worker 构造方法中的线程池工厂创建线程,并封装成工作线程Worker 对象
w = new Worker(firstTask);
// 注意这是Worker 中的属性对象 thread (第6处)
final Thread t = w.thread;
if (t != null) {
// 在进行ThreadPoolExecutor 的敏感操作时都需要持有住锁,避免在添加和启动线程时被干扰
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// 持有锁时重新检,在ThreadFactory失败或在获取锁之前关闭时退出。
int rs = runStateOf(ctl.get());
// 当线程池状态为RUNNING 或 SHUTDOWN 且firstTask 初始线程为空时
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // 预先检查t是否可启动
throw new IllegalThreadStateException();
workers.add(w);
int s = workers.size();
// 整个线程池在运行期间的最大并发任务个数
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
// 终于看到亲切的start 方法了,注意:并非线程池的execute 的command 参数指向的线程
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
// 线程启动失败,把刚才第3 处加上的工作线程计数再减回去
addWorkerFailed(w);
}
return workerStarted;
}
-
第1处:
配合循环语句出现的label,类似于goto 作用。label 定义时,必须把标签和冒号的组合语句紧紧相邻定义在循环体之前,否则会编译出错。目的是在实现多重循环时能够快速退出到任何一层。这种做法的出发点似乎非常贴心,但是在大型软件项目中,滥用标签行跳转的后果将是灾难性的。示例代码中,在retry 下方有两个无限循环,在workerCount 加1 成功后,直接退出两层循环。 -
第3处:
与第1处相呼应,AtomicInteger 对象的加1 操作时原子性的。break retry 表示直接跳出与retry 相邻的这个循环体。 -
第4处:
此continue 跳转至标签处,继续执行循环。如果条件为假,则说明线程池还处于运行状态,即继续在for(;;)循环内执行。 -
第5处:
compareAndIncrementWorkerCount 方法执行失败的概率非常低。即使失败,再次执行时成功的概率也是极高的,类似于自旋锁原理。这里的处理逻辑是先加1,创建失败再减1,这是轻量处理并发创建线程的方式。如果先创建线程,成功再加1,当发现超出限制后再销毁线程,那么这样的处理方式明显比前者代价要大。 -
第6处:
Worker 对象是工作线程的核心类实现,部分源码如下:
private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{
final Thread thread;
Runnable firstTask;
volatile long completedTasks;
Worker(Runnable firstTask) {
// 它是AbstractQueueSynchronizer 的方法,在runWorker 方法执行之前禁止线程被中断
setState(-1); // 禁止中断,直到runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
// 当thread 被start() 之后,执行runWorker 的方法
public void run() {
runWorker(this);
}
}
线程池的相关源码比较精炼,还包括线程池的销毁、任务提取和消费等,与线程状态图一样,线程池也有自己独立的状态转化流程。
总结
使用线程池要注意如下几点:
- 合理设置各类参数,应根据实际业务场景来设置合理的工作线程数。
- 线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
- 创建线程或线程池时请指定有意义的线程名称,方面出错时回溯。
线程池不允许使用Executors,而是通过ThreadPoolExecutor 的方式创建,这样的处理方法能更加明确线程池的运行规则,规避资源耗尽的风险。