ThreadPoolExecutor源码分析(下编)
文章目录
- 在ThreadPoolExecutor源码分析(上编)中,我们知道调用线程池线程执行的方法是execute()和submit()
- 线程在执行的过程中存在着五种状态的切换,线程的状态以及工作的线程数透过源码分析,知道是由ctl这个属性进行计算得出的
- 并且对execute()源码进行了分析,那么其中addWorker( )是添加当前线程到工作线程中执行线程,具体又是怎么执行的呢?下面我们接着往下看。
一. 解析添加工作线程的 addWorker方法
- 主要业务:添加工作线程,并启动工作线程
private boolean addWorker(Runnable firstTask, boolean core) {
//这个是定义一个内层for循环,跳出外层for循环的一个标识
retry:
//这里对线程池“状态”的判断,以及对工作线程“数量”的判断,成功则添加失败则执行再次尝试操作
for (;;) {
// 获取ctl值
int c = ctl.get();
// 拿到线程池的状态
int rs = runStateOf(c);
// 判断线程池的状态是否为RUNNING(如果不是,再次做后续的判断,查看当前任务是否可以不处理)
if (rs >= SHUTDOWN &&
// 如果为SHUTDOWN状态,且满足addwork(null,false),且工作任务队列不为空,则添加工作线程失败
! (rs == SHUTDOWN && firstTask == null && ! workQueue.isEmpty()))
return false;
//内层for循环判断工作线程数量
for (;;) {
//基于ctl获取当前工作线程数量
int wc = workerCountOf(c);
//判断工作线程是否大于最大值
if (wc >= CAPACITY ||
//判断工作线程和核心线程或者最大线程数量的大小
wc >= (core ? corePoolSize : maximumPoolSize))
//说明当前工作线程已经达到最大值了
return false;
//没有达到最大值,则自增工作线程数
//以CAS的方式,对工作线程+1
if (compareAndIncrementWorkerCount(c))
//如果成功则跳出外层for循环
break retry;
//当有并发的操作,则需要重新获取ctl的值
c = ctl.get(); // Re-read ctl
//基于新获取的ctl拿到线程池状态,判断和之前的rs状态是否一致
if (runStateOf(c) != rs)
//说明并发操作导致线程池的状态变化,需要重新判断状态
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
//添加工作线程,并启动工作线程
//工作线程是否启动
boolean workerStarted = false;
//工作线程是否添加了
boolean workerAdded = false;
//worker就是工作线程
Worker w = null;
try {
//构建工作线程,将任务扔到了worker对象中
w = new Worker(firstTask);
//获取worker中绑定的Thread线程
final Thread t = w.thread;
if (t != null) {
//加锁...避免添加工作线程的时候,突然把线程执行shutdown操作
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// 基于重新获取的ctl,拿到线程池状态
// 再次去判断了线程的状态,避免加锁前线程状态被改变)
int rs = runStateOf(ctl.get());
// 这里表示如果满足线程状态为RUNNING,就添加工作线程
if (rs < SHUTDOWN ||
// 如果线程池状态为SHUTDOWN,并且传入的任务为null
(rs == SHUTDOWN && firstTask == null)) {
// 则开始添加线程
// 这里是判断当前线程是否为run状态
if (t.isAlive())
throw new IllegalThreadStateException();
// 将构建好的work对象添加到workers(其实就是一个Hashset集合)
workers.add(w);
// 获取工作线程个数
int s = workers.size();
// 如果现在的工作线程数,大于历史最大的工作线程数
if (s > largestPoolSize)
// 重新赋值给largestPoolSize
largestPoolSize = s;
// 最后将工作线程添加,设置为true
workerAdded = true;
}
} finally {
// 释放锁
mainLock.unlock();
}
if (workerAdded) {
//工作线程添加成功,启动线程
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
// 表示,如果启动工作线程失败,调用这个方法来对添加工作线程失败补救
// (可理解为回滚到没添加线程之前的操作)
addWorkerFailed(w);
}
return workerStarted;
}
二. Worker对象(实际上就是工作线程)
private final class Worker
extends AbstractQueuedSynchronizer // 处理线程中断的情况
implements Runnable{ // 存储需要执行的任务
private static final long serialVersionUID = 6138294804551838833L;
// 工作线程对象,初始化时候构建出来的
final Thread thread;
// 需要执行的任务
Runnable firstTask;
Worker(Runnable firstTask) {
setState(-1); // 设置状态为-1,是为了让工作线程不被中断
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
// 当调用t.start(),执行当前的run方法
public void run() {
runWorker(this);
}
// 中断线程不是立即让线程停止,只是将thread的中断标识设置为true
protected boolean isHeldExclusively() {
return getState() != 0;
}
protected boolean tryAcquire(int unused) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
protected boolean tryRelease(int unused) {
setExclusiveOwnerThread(null);
setState(0);
return true;
}
public void lock() { acquire(1); }
public boolean tryLock() { return tryAcquire(1); }
public void unlock() { release(1); }
public boolean isLocked() { return isHeldExclusively(); }
void interruptIfStarted() {
Thread t;
if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
try {
t.interrupt();
} catch (SecurityException ignore) {
}
}
}
}
三. runWorker方法(实际上就是执行任务的流程)
- 主要业务:执行任务的流程,并且做了中断线程的相关判断
final void runWorker(Worker w) {
// 当前工作线程
Thread wt = Thread.currentThread();
// 拿到worker对象中封装的任务
Runnable task = w.firstTask;
// 然后将firstTask设置为null
w.firstTask = null;
// 将worker的stater设置为0,让其可以被中断
w.unlock();
boolean completedAbruptly = true;
try {
// 获取任务的两种方式:
// 1. 执行execute/submit时,传入的任务直接处理
// 2. 从工作队列中获取任务执行
while (task != null || (task = getTask()) != null) {
// 加锁,在SHUTDOWN状态下,当前线程不允许被中断
w.lock();
// 如果线程池状态变为了STOP状态,必须将当前线程中断
// 第一个判断:判断当前线程池状态是否为STOP
// 第二个判断:查看中断标记是否为false,如果为false,说明不是STOP;
// 如果为true,则需要再次查看是否并发操作导致线程为STOP
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP)))
// 查询当前线程中断标记是否为false
// 是false, 则执行wt.interrupt();
&& !wt.isInterrupted())
// 将中断标记设置为true
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
task = null;
// 执行成功的任务个数+1
w.completedTasks++;
// 将state标记位设置为0
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
四. getTask方法
- getTask( ) 其实就是从工作队列 workQueue 中获取到任务的方法
private Runnable getTask() {
// 这只是个标识(非核心线程可以干掉)
boolean timedOut = false; // Did the last poll() time out?
for (;;) {
//========判断线程池状态========
// 获取ctl标识
int c = ctl.get();
// 拿到线程池的状态
int rs = runStateOf(c);
// 如果满足条件,则工作线程减一
// 第一个判断:线程状态为SHUTDOWN,STOP,且线程池状态大于等于STOP
// 第二个判断:线程池状态为SHUTDOWN,且工作队列为空
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
//移除当前工作线程
decrementWorkerCount();
// 返回null,交给processWorkerExit移除当前线程
return null;
}
//========判断工作线程数量========
// 获得工作线程数
int wc = workerCountOf(c);
// allowCoreThreadTimeOut 表示是否允许核心线程超时(默认为false)
// 第二个判断是:工作线程是否大于核心线程
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
// 工作线程数是否已经大于最大线程数
// 工作线程数大于核心线程数,并且当前线程已经超时,则去除当前线程
if ((wc > maximumPoolSize || (timed && timedOut))
// 如果工作线程数大于1,或者工作队列为空,则去除当前线程
&& (wc > 1 || workQueue.isEmpty())) {
// 基于CAS方式去除线程
if (compareAndDecrementWorkerCount(c))
// 返回null,交给processWorkerExit移除当前线程
return null;
continue;
}
//========从工作队列中获取任务========
try {
Runnable r = timed ?
// 阻塞到一定时间,从工作队列拿任务
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
// 一直阻塞
workQueue.take();
if (r != null)
// 如果拿到任务直接返回执行
return r;
// r为空,说明从队列中获取任务超时了
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
五. processWorkerExit方法
- 移除当前工作线程的操作
private void processWorkerExit(Worker w, boolean completedAbruptly) {
if (completedAbruptly)
// 进入这个方法,主要是因为线程执行异常(勾子函数)引起的
decrementWorkerCount();
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// 记录当前线程池一共处理了多少个任务
completedTaskCount += w.completedTasks;
// 移除工作线程
workers.remove(w);
} finally {
mainLock.unlock();
}
// 尝试将线程池关系(转换到过渡状态-->销毁状态 )
tryTerminate();
int c = ctl.get();
// 当前线程池状态说明是RUNNING,SHUTDOWN
if (runStateLessThan(c, STOP)) {
// 如果是正常状态则移除当前工作线程
if (!completedAbruptly) {
// 核心线程数最小值
int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
// 如果工作队列中的任务不为空,设置工作线程最小值为1
if (min == 0 && ! workQueue.isEmpty())
min = 1;
// 如果工作线程在线程池中,则直接返回
if (workerCountOf(c) >= min)
return; // replacement not needed
}
// 程序走到这里,说明是不正常的方式移除了工作线程,则需要添加个空任务的工作线程去执行
addWorker(null, false);
}
}
以上是我们对ThreadPoolExecutor的核心源码进行分析,了解了线程是怎么去添加线程,移除线程,以及执行线程的流程,那么现在回到开头,一起回答下这几个问题?
思考
1. ThreadPoolExecutor线程池大小设置?
-
线程池的理想大小取决于被提交任务的类型以及所部署系统的特性。
-
线程池应该避免设置的过大或过小,如果线程池过大,大量的线程将在相对很少的CPU和内存资源上发生竞争,这不仅会导致更高的内存使用量,而且还可能耗尽资源。
-
如果线程池过小,那么将导致许多空闲处理器无法执行任务,降低了系统吞吐率。
-
因此需要分析任务的特性,去决定设置线程池的大小:
任务的性质分为:CPU密集型,IO密集型,混合型
任务等级分为:高,中,低
任务执行时间分为:长,中,短
任务的依赖性分为:是否依赖其他系统资源,如数据库链接等。
-
根据性质不同的任务可以交给不同规模的线程池去执行
CPU密集型任务:应配置尽可能小的线程,如配置CPU个数+1的线程数
IO密集型任务:应配置尽可能多的线程,因为IO操作不占用CPU,不要让CPU闲下来,应加大线程数量,如配置两倍CPU个数+1
混合型的任务:如果可以拆分,拆分成IO密集型和CPU密集型分别处理,前提是两者运行的时间是差不多的,如果处理时间相差很大,则没必要拆分了。
若任务对其他系统资源有依赖,如某个任务依赖数据库的连接返回的结果,这时候等待的时间越长,则CPU空闲的时间越长,那么线程数量应设置得越大, 才能更好的利用CPU。
-
有个估算线程池大小合理值的公式
最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 ) CPU数目*
比如:平均每个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,比如IO)为1.5s,CPU核心数为8,那么根据上面这个公式估算得到:((0.5+1.5)/0.5)*8=32。
这个公式进一步转化为:最佳线程数目 = (线程等待时间与线程CPU时间之比 + 1) CPU数目*
**可以得出一个结论:**线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。
2. 阻塞队列怎么设置比较合适?
通过源码了解知道阻塞队列实现类(常用的)有:
ArrayBlockingQueue 是一个由数组结构组成的有界阻塞队列,是先进先出的原则进行排序。
LinkedBlockingQueue是一个由链表结构组成的有界阻塞队列,如果不制定大小,默认是Interger.MAX_VALUE, 先进先出原则。
SynchronousQueue是一个不存储元素的阻塞队列。每一个put操作必须等待一个take操作,否则不能继续添加元素。线程池中用这个队列,必须有一个线程可以执行队列中获取的任务。
DelayQueue是一个支持延时获取元素的无界阻塞队列。队列使用PriorityQueue来实现。队列中的元素必须实现Delayed接口,在创建元素时可以制定多久才能从队列中获取当前元素。只有在时间到时,才能从队列中获取元素。
通过了解其特性,根据实际任务的应用场景进行选择即可。
参考文献:[ThreadPoolExecutor线程池大小设置](https://www.6miu.com/read-2402603.html)
以上内容是在【马士兵教育】源码剖析深入讲解Java多线程并发实现原理系统教程的学习总结,感兴趣的同学可以去B站搜索,了解一下哦!!!