一、线程池介绍
Java5开始,在util下提供了一个包,叫做JUC(java.util.concurrent),里面提供了关于多线程、并发的一些工具包。例如锁、多线程等工具都在这个包中。
我们知道一个线程的创建、销毁过程是会消耗系统性能的,需要使用cpu,占用内存,当频繁大量的创建销毁线程,这个消耗累积到会影响系统性能,为了解决这一类问题,出现了“池化”的概念,“池化”意为将资源放进一个池子内,需要的时候就从池中取,不用的时候就返还池中以便其它复用,实践的例子很多,例如数据库连接池、缓冲池以及本次讲解的线程池等,中心思想就是复用这些连接,避免频繁的创建销毁,提高性能。
juc中的线程池的类关系如下图:
Executor.class是顶层接口类,只定义了一个方法:execute()方法,入参为Runnable接口;
ExecutorService.class是继承Executor的接口,加入了一些方法submit()、shutdown()、shutdownNow()等方法的定义;
AbstractExecutorService抽象类实现了ExecutorService部分接口,比如submit()等方法;
ScheduleExecutorService接口继承ExecutorService,顾名思义,增加了一些计划执行函数,实现周期行执行任务的功能;
ThreadPoolExecutor类就是我们常用的线程池工具类了,它继承于抽象类AbstractExecutorService,实现了线程池的任务、线程以及队列等功能,接下来我们详细介绍ThreadPoolExecutor的功能实现。
二、线程池源码
如何使用线程池?假设我们有如下图线程池实例代码,可以看到大致创建线程池并且启动的大概逻辑为创建,执行:
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ExecutorTest {
public static void main(String[] args) {
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Running----");
}
}
//创建线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5,
10,
10,
TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(10), new ThreadPoolExecutor.DiscardPolicy());
//加入执行任务
executor.submit(new MyRunnable());
}
}
线程池构造参数含义?可以看到创建一个线程池,构造方法中需要传入很多参数,每个参数都有很重要的含义,下表列出了每个参数的含义:
参数名称 | 含义 | 备注 |
int corePoolSize | 核心线程 | 线程池核心线程数,包括空闲线程 |
int maximumPoolSize | 最大线程数 | 线程池所允许的最大线程数量 |
long keepAliveTime | 线程存活时间 | 线程空闲时间超过该值则销毁(对核心线程起作用需配置参数) |
TimeUnit unit | 时间单位 | 线程空闲存活时间单位 |
BlockingQueue<Runnable> workQueue | 阻塞队列 | 当核心线程用尽,将新任务加入队列 |
ThreadFactory threadFactory | 线程工厂 | 创建线程的工厂 |
RejectedExecutionHandler handler | 拒绝策略 | 当队列满时,根据该策略处理新到达的任务 |
注:boolean allowCoreThreadTimeOut决定keepAliveTime是否对核心线程有作用;
ThreadPoolExecutor提供了四种拒绝策略:
- 1.AbortPolicy:默认策略,丢弃任务,并且抛出RejectedExecutionException;
- 2.DiscardPolicy:丢弃任务,什么都不做;
- 3.DiscardOldestPolicy:丢弃老任务,执行该新任务;
- 4.CallerRunsPolicy:调用者线程执行该任务;
注:也可以自定义拒绝策略,实现RejectedExecutionHandler接口。
ThreadPoolExecutor提供四种基础阻塞队列:顶层是BlockingQueue接口
- 1.ArrayBlockingQueue:有界阻塞队列,底层为数组;
- 2.LinkedBlockingQueue:无界或有界阻塞队列,底层为链表;
- 3.SynchronousQueue:不存储元素,一个线程执行元素插入,需等待另一个线程执行移除元素,否则阻塞插入操作;
- 4.PriorityBlockingQueue:无界的,带有优先级的阻塞队列;
注:队列一般需要设置上限,需要注意无界队列,如果过多的任务堆积于队列中,有oom风险。
ThreadPoolExecutor线程池状态流转:
线程池中基本成员参数?在线程池中维护了一些参数,例如状态、线程组以及锁等信息,如下图所示:
//32位,前3位代表线程池状态,后29位代表线程池数量
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
//线程数量位32 - 3 = 29
private static final int COUNT_BITS = Integer.SIZE - 3;
//线程数容量000 11111111111111111111111111111
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
//各个运行状态标志(值:running<~<terminated)
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;
//获取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; }
//阻塞队列,任务容器
private final BlockingQueue<Runnable> workQueue;
//锁,增加线程数、完成任务数等使用
private final ReentrantLock mainLock = new ReentrantLock();
//线程的容器
private final HashSet<Worker> workers = new HashSet<Worker>();
//锁的条件功能
private final Condition termination = mainLock.newCondition();
//线程数量峰值
private int largestPoolSize;
//完成的任务数量
private long completedTaskCount;
//创建线程的工厂
private volatile ThreadFactory threadFactory;
//拒绝策略句柄
private volatile RejectedExecutionHandler handler;
//空闲线程存活时间
private volatile long keepAliveTime;
//是否主线程也有过期时间
private volatile boolean allowCoreThreadTimeOut;
//核心线程数量
private volatile int corePoolSize;
//最大线程数量
private volatile int maximumPoolSize;
线程池启动过程?当我们创建一个线程池后,添加并执行任务时:
1.首先判断当前线程的数量是否小于核心线程数量,如果小于,则直接创建核心线程并执行此任务,如果大于等于,则尝试把此任务加入到阻塞队列;
2.如果加入阻塞队列成功,则等待空闲线程来阻塞队列拉取任务并执行;
3.如果加入阻塞队列失败,则说明队列已经满了,这时候需要判断当前线程数量是否小于最大线程数,如果小于,则创建非核心线程并执行该任务;
4.如果大于等于最大线程数,则使用拒绝策略处理该任务;
线程池的大概执行逻辑如上步骤,接下来我们看代码。我们知道,线程池的入口执行方法是submit()以及execute(),前者实际上也是调用的后者,因此我们从execute()方法入口开始介绍执行过程:
public void execute(Runnable command) {
if (command == null)//传入的任务为null,直接抛出空指针异常
throw new NullPointerException();
int c = ctl.get();//获取ctl,拿到线程池状态、线程数量
//1.获取线程数量,和核心线程数量比较
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))//小于核心线程,则创建核心线程,并且把该任务加入到该线程
return;
c = ctl.get();//创建核心线程失败(可能线程池非运行状态、核心线程刚满),重新获取ctl
}
//2.如果超出核心线程数,尝试加入队列
if (isRunning(c) && workQueue.offer(command)) {//运行状态,加入队列成功
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))//二次检查
reject(command);//如果非运行状态,队列移除该任务,拒绝策略处理
else if (workerCountOf(recheck) == 0)//检查是否有线程,如果没有,创建空线程
addWorker(null, false);
}
//3.非运行或者队列满,则创建非核心线程执行该任务
else if (!addWorker(command, false))
reject(command);//超出最大线程数量或非运行状态,拒绝策略处理
}
可以看出主要判断逻辑为:1.核心线程是否满;2.队列是否满;3.是否超过最大线程数;
上述过程的流程图如下图所示:
上述过程中,一个重要的方法addWorker()是新建一个worker,并把任务放入该worker中执行,下面我们分析该方法的执行过程:
private boolean addWorker(Runnable firstTask, boolean core) {
//1.增加线程数量
retry://goto 标识
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))//如果线程池非运行状态,且是停止状态、任务为空、队列为空,则返回false,添加失败
return false;
for (;;) {
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))//规则校验线程数量
return false;
if (compareAndIncrementWorkerCount(c))//CAS增加线程数量
break retry;//增加线程数量成功,跳出外循环
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)//增加线程数量失败,线程池状态改变则开始外循环,状态未变则开始内循环
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
//2.新建worker,执行线程任务
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
int rs = runStateOf(ctl.get());
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();
if (s > largestPoolSize)
largestPoolSize = s;//更新线程数峰值
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
当在addWorker()方法中添加成功后,执行了start()函数,会去调用Worker类的run()方法,run()方法调用runWoker()方法,执行过程如下:
//Woker方法重写的run()方法
public void run() {
runWorker(this);
}
//Worker的任务执行方法
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
while (task != null || (task = getTask()) != null) {//取本身任务、从队列获取任务;getTask方法很重要!!!
w.lock();
// If pool is stopping, ensure thread is interrupted;
// if not, ensure thread is not interrupted. This
// requires a recheck in second case to deal with
// shutdownNow race while clearing interrupt
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())//线程池状态大于等于停止,线程是中断状态
wt.interrupt();
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++;//完成的任务数量自增
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
worker执行最后会对该线程做相应处理,包括cas增加完成任务数、尝试终止线程池、是否补线程等操作:
private void processWorkerExit(Worker w, boolean completedAbruptly) {
if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted
decrementWorkerCount();//意外情况直接减少线程数量
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
completedTaskCount += w.completedTasks;//cas增加完成任务数
workers.remove(w);//移除该worker
} finally {
mainLock.unlock();
}
tryTerminate();//尝试终止线程
int c = ctl.get();
//如果线程运行中,且正常结束,判定是否需要补充线程
if (runStateLessThan(c, STOP)) {
if (!completedAbruptly) {
int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
if (min == 0 && ! workQueue.isEmpty())
min = 1;
if (workerCountOf(c) >= min)
return; // replacement not needed
}
addWorker(null, false);
}
}
线程池如何停止?在上方线程池状态流转图中可以看到,线程池可以通过shutdown()、shutdownNow()方法使线程池到达shutdown、stop停止状态,在运二者区别在于对于队列以及执行中的任务的处理方式,前者会等待队列及正在运行的任务完成才会执行退出逻辑,后者会终止正在运行的任务,剔除并返回所有队列中的任务,是比较粗暴的,下面介绍两种结束方式:
shutdownNow()方法执行逻辑?设置线程池状态为STOP,中断所有未中断线程,移除所有任务并返回,
public List<Runnable> shutdownNow() {
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
advanceRunState(STOP);//CAS设置状态为STOP
interruptWorkers();//中断所有线程
tasks = drainQueue();//移除所有任务,并返回
} finally {
mainLock.unlock();
}
tryTerminate();
return tasks;
}
中断方法是比较粗暴的:
private void interruptWorkers() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
for (Worker w : workers)
w.interruptIfStarted();
} finally {
mainLock.unlock();
}
}
void interruptIfStarted() {
Thread t;
if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
try {
t.interrupt();
} catch (SecurityException ignore) {
}
}
}
对于队列中未执行的任务,直接抛弃并返回,那对于正在执行的任务是如何处理呢?文章中部图runWoker()方法中可以看到循环获取当前线程和阻塞队列里的线程,然后加锁、执行、结束放锁。运行中的线程改变了中断标志,如果处于阻塞状态(IO阻塞)则会抛异常,然后结束本线程,如果是正常执行线程,则执行完毕后退出。
如果在getTask()方法返回null,则线程完毕,方法如下,可以看到开始会判断线程池的状态,shutdownNow()此时已经将下线程池标注为STOP状态了。
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
int wc = workerCountOf(c);
// Are workers subject to culling?
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try {
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
shutdown()方法执行逻辑?相比于shutdownNow()方法,则更加优雅一些,代码如下:
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
advanceRunState(SHUTDOWN);
interruptIdleWorkers();
onShutdown(); // hook for ScheduledThreadPoolExecutor
} finally {
mainLock.unlock();
}
tryTerminate();
}
中断方法如下,可以看到有一个加锁操作,而我们反过头来看到,runWorker()方法,执行中的方法先获取到锁了,因此执行中的任务,这里是无法终止的。
private void interruptIdleWorkers(boolean onlyOne) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
for (Worker w : workers) {
Thread t = w.thread;
if (!t.isInterrupted() && w.tryLock()) {//非中断,并且尝试加锁成功
try {
t.interrupt();
} catch (SecurityException ignore) {
} finally {
w.unlock();
}
}
if (onlyOne)
break;
}
} finally {
mainLock.unlock();
}
}
三、固定线程池
juc包中提供了一个类Executors,它提供了一些特殊固定功能的线程池,主要包括四种:
1.newSingleThreadPool:单一线程池,有且仅有一个线程;LinkedBlockingQueue是无界阻塞队列,队列可存放的大小为Integer.MAX_VALUE
2.newFixedThreadPool:固定数量线程的线程池,LinkedBlockingQueue是无界阻塞队列,队列可存放的大小为Integer.MAX_VALUE;
3.newCachedThreadPool:缓冲线程池,核心线程0,最大线程池数量是Integer.MAX_VALUE,空闲时间60s,使用SynchronousQueue,该队列不存储值;
4.newScheduledThreadPool:定时调度的线程池;
四、注意事项
1.合理配置线程数
线程数的多少直接关乎到线程池的性能效率以及对系统的影响,线程数量配置过多,一方面增加系统负载,另一方面如果任务不多会浪费线程资源,如果过少,任务过多怎会进入队列,可能延迟完成时间。首先需要分析我们的业务是IO密集型还是CPU密集型,根据实际业务情况,配置合理的线程数。
IO密集型业务主要是阻塞进行IO操作,比较耗时,因此需要配置多的线程数,弥补阻塞的时间,一般配置为CPU核数*2;
CPU密集型业务是进行大量的运算,多核的CPU可以提高计算速度,少量配置线程数,这样增加多核计算时间几率,因为相比之下线程切换可能更加耗时,一般配置CPU核数+1的线程数量;
上面的建议只是一个参考的方向,实践中还需要考虑业务的并发量,每个任务的执行时间,以及任务执行时间和利用cpu的时间比例,结合来考量线程数量的配置。
2.优雅关闭线程池
线程池中提供了两种停止线程池的方法:shutdown()、shutdownNow(),两者执行后的线程池状态在上面图中已经描述了,为了保证任务合理的关闭,我们应该选取第一种方式关闭,停止接收外部任务,执行完阻塞队列和正在执行的任务,然后,在通过方法awaitTermination()阻塞主线程,等待子线程任务完成后,优雅的关闭。
3.谨慎使用Executors提供的特殊线程池
Executors提供的几种特殊线程池,虽然省去了传入部分参数的麻烦,但是里边是有一部分的风险的。
单一或者固定数量的线程池,使用几乎无界的阻塞队列,如果任务执行很慢,但是任务很多,这个队列会急剧增加,可能会占用大量内存,并且因为在使用无法剔除,导致oom。
缓冲线程池,队列不会存储任务,当任务量急剧增多,会瞬间创建大量的线程,这个也是很危险的。虽然我们前面已经根据业务评估使用不同的线程池,但是这些风险还是存在的,因此估计使用自定义的线程池。
五、资源地址
文档:《Thinking in java》jdk1.8版本源码