浅谈线程池ThreadPoolExecutor
原文链接:https://juejin.im/post/5aabb948f265da237506a7f5
案例:抄写员
在中世纪,有一种叫做抄写员(Scribe)的工作,他们的职责就好像是复印机,抄写一本又一本书。假如这个时候有一个抄写员工作室,只有2个抄写员,他们要抄写10本书。
我们在本例中分别“自己写线程管理”和“由ThreadPoolExecutor做线程管理”
public static class Book {
private static AtomicInteger id = new AtomicInteger(0); // 书名生成器
private String bookName; // 书名
public void copy() { // 抄写书籍
System.out.println("start copy " + bookName);
try {
Thread.sleep(100L); // sleep 100ms
} catch (Exception e) {
// ignore
}
System.out.println("end copy " + bookName);
}
public Book() {
bookName = "book-" + String.valueOf(id.incrementAndGet()); // 书名自动生成
}
}
自己实现线程管理
// 提前准备好十本书
final BlockingQueue<Book> books = new LinkedBlockingDeque<Book>(10);
for (int i = 0; i < 10; i++) {
try {
books.put(new Book());
} catch (Exception e) {
// ignore
}
}
System.out.println("start work...");
// 创建两个书籍抄写员线程
Thread[] scribes = new Thread[2];
for (int scribeIndex = 0; scribeIndex < 2; scribeIndex++) {
scribes[scribeIndex] = new Thread(new Runnable() {
public void run() {
for (; ; ) {
if (Thread.currentThread().isInterrupted()) {
System.out.println("time arrives, stop writing...");
}
try {
Book currentBook = books.poll(5, TimeUnit.SECONDS);
currentBook.copy();
} catch (Exception e) {
System.out.println("time arrives, stop writing...");
return;
}
}
}
});
scribes[scribeIndex].setDaemon(false); // 设置为非守护线程
scribes[scribeIndex].start();
}
// 工作已经安排下去了,安心等待就好了
try {
Thread.sleep(10000l);
} catch (Exception e) {
// ignore
}
// 时间到了,提醒两个抄写员停止抄写
for (int scribeIndex = 0; scribeIndex < 2; scribeIndex++) {
scribes[scribeIndex].interrupt();
}
System.out.println("end work...");
复制代码
写了一大堆代码来完成上述的功能,让我们一起来看看用了ThreadPoolExecutor是怎么完成的。
System.out.println("start work...");
ExecutorService executorService = Executors.newFixedThreadPool(2);
for (int i = 0; i < 10; i ++) {
executorService.submit(new Runnable() {
public void run() {
new Book().copy();
}
});
}
// 工作已经安排下去了,安心等待就好了
try {
Thread.sleep(10000l);
} catch (Exception e) {
// ignore
}
executorService.shutdownNow();
System.out.println("end work...");
整个流程非常清晰,分别是:任务编写、线程创建、线程启动、终止线程。
但是很多时候,问题并不仅限于上述的内容。
开发者困境
最早的并发编程的开发者很多事情都需要亲力亲为,而通过使用Java线程池,可以完成以下工作:
- 1)线程管理,线程的创建、启动、销毁等工作;
- 2)线程复用,线程的创建是会给服务器带来一定开销的,如何减少频繁重复创建线程的开销;
- 3)弹性伸缩,服务器通常有高峰期也有低峰期,线程池是否可以弹性伸缩,比如线程创建成功后长时间不使用是否可以回收,以减少系统资源的浪费,或者线程池的容量是否可以随时增长;
- 4)拒绝策略,线程数量有限而需要处理的任务很多,超出系统承载范围的任务是拒绝还是阻塞等待;
- 5)异常处理,线程在执行过程中可能遇到异常或者错误,开发者如何正确应对这些异常或者错误;
- 6)任务分配,任务的分配是基于先入先出还是基于某种优先级的策略。
等等如是,不一而足,这个时候我们就要介绍Doug Lea大神开发的ThreadPoolExecutor线程池框架,看看大神是怎么解决上面这些问题的。
ThreadPoolExecutor源码简析
首先,在解读源码之前,要引入ThreadPoolExecutor的一些重要概念
生命周期
在ThreadPoolExecutor线程池的设计中,把整个任务执行框架线程池划分为5个生命周期:
- RUNNING:允许接收新任务并且处理队列中的任务
- SHUTDOWN:不再接收新的任务,仅消化完队列中的任务
- STOP:不仅不再接收新的任务,连队列中的任务都不再消化处理了,并且尝试中断正在执行任务的线程
- TIDYING:所有任务被终止了,工作线程数workCount也被设为0,线程的状态也被设为TIDYING,并开始调用钩子函数terminated()
- TERMINATED:钩子函数terminated()执行完毕
各个生命周期的转化图如下:
从图中可以看到,整个生命周期的变化是不可逆的。
状态字
ThreadPoolExecutor把线程池状态和线程池容量打包成一个int型变量
线程池状态位
状态 高位值枚举 正负性
RUNNING 111 负数(-536870912)
SHUTDOWN 000 0
STOP 001 正数(536870912)
TIDYING 010 正数(1073741824)
TERMINATED 011 正数(1610612736)
因此在状态值的排布上可以知道 TERMINATED > TIDYING > STOP >SHUTDOWN >RUNNING
ThreadPoolExecutor中的代码如下所示:
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
// 状态字的高比特位存放线程池状态信息
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
// 打包/提取状态字信息
private static int runStateOf(int c) { return c & ~CAPACITY; }
private static int workerCountOf(int c) { return c & CAPACITY; }
// 判断当前线程池是否正在执行
private static boolean isRunning(int c) {
return c < SHUTDOWN;
}
线程池主要执行流程
1.线程池创建
ExecutorService executorService = Executors.newFixedThreadPool(2);
这里使用了Executors提供的工厂方法,可以创建以下四种类型线程池:
newFixedThreadPool。该方法将用于创建一个固定大小的线程池(此时corePoolSize = maxPoolSize),每提交一个任务就创建一个线程池,直到线程池达到最大数量,线程池的规模在此后不会发生任何变化;
newCachedThreadPool。该方法创建了一个可缓存的线程池,(此时corePoolSize = 0,maxPoolSize = Integer.MAX_VALUE),空闲线程超过60秒就会被自动回收,该线程池存在的风险是,如果服务器应用达到请求高峰期时,会不断创建新的线程,直到内存耗尽;
newSingleThreadExecutor。该方法创建了一个单线程的线程池,该线程池按照任务在队列中的顺序串行执行(如:FIFO、LIFO、优先级);
newScheduledThreadPool。该方法创建了一个固定长度的线程池,可以以延迟或者定时的方式执行任务;
2、任务提交
任务提交的大概逻辑如下:
- 1)当线程池小于corePoolSize时,新提交任务将创建一个新线程执行任务,即使此时线程池中存在空闲线程;
- 2)当线程池达到corePoolSize时,新提交任务将被放入workQueue中,等待线程池中任务调度执行;
- 3)当workQueue已满,且maximumPoolSize > corePoolSize时,新提交任务会创建新线程执行任务;
- 4)当提交任务数超过maximumPoolSize时,新提交任务由RejectedExecutionHandler处理;
- 5)当线程池中超过corePoolSize线程,空闲时间达到keepAliveTime时,关闭空闲线程;
那么接下来我们看看源代码是怎么实现上面的描述的
线程池创建成功以后,我们提交任务到线程池中:
executorService.submit(new Runnable() {
public void run() {
new Book().copy();
}
});
submit到线程池以后:
public <T> Future<T> submit(Runnable task, T result) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task, result);// 包装出一个新的任务
execute(ftask); // 线程池的入口
return ftask;
}
可以看到ThreadPoolExecutor的入口方法就是execute(Runnable commad)。该方法的执行逻辑如下:
int c = ctl.get();
// 1. 如果当前线程池中线程总数少于核心线程数,则添加新线程到线程池中,
// 并且由新线程执行刚提交进来的任务
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
// 2. 可能刚才在创建新线程成功的同时,线程池被关闭了,因此需要double-check,
// 如果此时线程池已经被关闭了,那么回滚刚才被添加进来的任务
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// 3. 如果此时核心线程数(corePoolSize)已经满了,并且任务队列也满了,
// 尝试增加线程到maximumPoolSize大小,如果仍然失败,执行拒绝策略
else if (!addWorker(command, false))
reject(command);
在上面的代码里面,ctl.get()方法、workerCountOf()、以及isRunning() 方法都是对之前提到的状态字进行读写的操作,这部分我们就不再展开给读者看了,有兴趣的读者可以自己了解一下。
接下来,我们看看addWorker都做了什么工作:
private boolean addWorker(Runnable firstTask, boolean core) {
// 这部分省略的代码都是对状态字进行修改,添加并创建线程之前,
// 需要递增work记数(此时需要线程安全地操作)
...
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
...
w = new Worker(firstTask); // 此处封装出了一个新的Work,这个类我们稍后会介绍
final Thread t = w.thread;
if (t != null) {
...
// 获得线程池状态,如果线程池已经被关闭了,则不再创建新的线程
int c = ctl.get();
int rs = runStateOf(c);
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
workers.add(w);
int s = workers.size();
...
workerAdded = true;
...
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
// 如果任务启动或者提交到线程池失败,
// 则执行回滚操作(从工作线程池中移除失败添加的worker、减少状态字中的任务计数)
addWorkerFailed(w);
}
return workerStarted;
}
3、任务执行
任务执行在Worker类中,而Worker类是一个继承了Runnable接口的类。
private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{
...
public void run() {
runWorker(this);
}
...
}
可以看到Worker类中调用了外部的runWorker()方法。因此可以了解到,任务执行的主要逻辑,就是在外部的runWorker()方法中执行的
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
...
boolean completedAbruptly = true;
try {
while (task != null || (task = getTask()) != null) { // 循环读取任务
...
try {
beforeExecute(wt, task); // 用户实现的回调方法,任务启动前
Throwable thrown = null;
try {
task.run();// 任务执行
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown); // 用户实现的回调方法,任务执行后
}
} finally {
task = null;
w.completedTasks++;
...
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
beforeExecute和afterExecute是两个钩子方法,在里面指定了当线程开始执行和完成执行以后执行的动作,需要开发者实现。
另外需要注意的还有runWorker方法内调用的getTask()方法,在该方法内部,如果发生以下情况将会返回null,终止工作线程的执行循环: 1)当前线程数即将超过maxPoolSize 2)线程池被关闭 3)当前线程数大于corePoolSize且小于maxPoolSize,并关切从BlockingQueue取数据超过了超时时间(默认60秒)
代码实现如下:
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// 校验当前线程池状态
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
boolean timed; // Are workers subject to culling?
for (;;) {
int wc = workerCountOf(c);
timed = allowCoreThreadTimeOut || wc > corePoolSize;
if (wc <= maximumPoolSize && ! (timedOut && timed))
break;
if (compareAndDecrementWorkerCount(c))
return null;
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
try {
// 如果线程超过指定时间内(默认60秒)没有获取到任务,说明有线程即将过期
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
4、任务拒绝
如果线程被提交到线程池时,当前线程池出现以下情况的任一一种情况: 1)线程池任务队列已经满了 2)线程池被关闭了(调用了shutdown函数或者shutdownNow函数) 都将会调用提前设置好的回调策略,ThreadPoolExecutor中总共提供了四种策略:
- 1)AbortPolicy(中止):该策略将会直接抛出RejectedExecutionException异常,调用者将会获得异常;
- 2)DiscardPolicy(抛弃):使用该策略,线程池将会悄悄地丢弃这个任务而不被调用者知道;
- 3)CallerRunsPolicy(调用者运行):该策略既不会抛弃任务也不会抛出异常,而是将这个任务退回给调用者,从而降低新任务的流量;
- 4)DiscardOldestPolicy(抛弃最旧的):该策略将会抛弃下一个即将轮到执行的任务,那么“抛弃最旧”的将导致抛弃优先级最高的任务,因此最好不要把“抛弃最旧的”饱和策略和优先级队列放在一起使用; 这里,代码实现我们将只展示**CallerRunsPolicy(调用者运行)**策略:
public static class CallerRunsPolicy implements RejectedExecutionHandler {
/**
* Creates a {@code CallerRunsPolicy}.
*/
public CallerRunsPolicy() { }
// 策略实现
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
}
当然开发者也可以选择,根据业务需求,定义自己的饱和策略。
5、线程池销毁
ThreadPoolExecutor提供了两种方法销毁线程池,分别是shutdown()和shutdownNow()
shutdown()方法仅仅是把线程池的状态置为SHUTDOWN,并且拒绝之后尝试提交进来的所有请求,但是已经在任务队列里的任务会仍然会正常消费。
而shutdownNow()方法的表现显得更加简单粗暴,它会强行关闭ExecutorService,也会尝试取消正在执行的任务,并且返回所有已经提交但尚未开始的任务,开发者可以将这些任务写入日志保存起来以便之后进行处理,另外尝试取消正在执行的任务仅仅是尝试对执行线程进行中断,具体的线程响应中断策略需要用户自己编写。代码实现如下:
public List<Runnable> shutdownNow() {
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
advanceRunState(STOP);
interruptWorkers();
tasks = drainQueue();
} finally {
mainLock.unlock();
}
tryTerminate();
return tasks;
}
小心踩坑:线程池经验谈
不要使用ThreadLocal
不要在ThreadPoolExecutor线程池中使用ThreadLocal,因为在ThreadPoolExecutor中,线程是复用的,因此在这里使用ThreadLocal会被多个task共享,因此可能会带来脏数据污染。需要小心使用
合理设置corePoolSize的值
以一段代码为例:
// 10个线程,因为任务多,这里用LinkedBlockingQueue
private static final LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>();
private static final ExecutorService service = new ThreadPoolExecutor(0, 10,
60L, TimeUnit.SECONDS, queue
);
代码中的corePoolSize=0,也就是核心线程数是1,如果任务数多于10个,那么会先创建maximumPoolSize个线程执行,其余的任务加入 queue 中等待执行。
而在ThreadPoolExecutor的实现中,当workQueue已满,且maximumPoolSize>corePoolSize时,新提交任务会创建新线程执行任务。
因此,queue 是不会满的,那么永远不会有maximumPoolSize个线程被创建,也就是说我们的任务一直还是一个线程在跑,无法达到可以同时使用多个线程的预期。
线程中断
虽然ThreadPoolExecutor提供了shutdownNow()方法,在调用该方法后会尝试中断所有线程,但是该中断并不能保证线程一定会就此终止,因此,需要开发者实现线程中断的策略。关于这部分的内容,在Doug Lea的《Java Concurrency In Practice》的7.1.2节已经进行了完整的讨论,笔者在这里就不再赘述了。
finalize函数
尤其需要注意的是,ThreadPoolExecutor有一个finalize函数,具体实现如下:
protected void finalize() {
shutdown();
}
在该方法中调用了shutdown()函数,因此,如果你并不是真正希望停止线程池的执行,那么就不要让线程池离开你代码的作用范围。
更多推荐
所有评论(0)