注:环境为mac os x 10,jdk8
0 前言
其实将ThreadPoolExecutor源码分析放在java基础内,个人还是有点犹豫的,权当java基础知识的巩固吧。
废话少说,放码过来。
1、准备知识
磨刀不误砍柴工,先看点准备知识,后面不懂再来看也ok。
1.1 位运算
或、与、非、异或(相同0,不同1,等同于不进位的加法);
带符号左右移:>>、<<,符号位不参与移位运算。
无符号左右移:>>>、<<<,符号位参与移位运算。
移位运算,不足时补0.
1.2 java底层数据存储
这里只讲整形数据,浮点类型在ThreadPoolExecutor内目前不涉及。
以32位jdk为例,第一位为符号位,作用为-1的幂次方,为0表示为整数,为1表示为负数,后面31位为2进制数,这样能表示的最大整数是2^31,最小负数是-2^31-1。其中java底层存储方式是:正数为原码存储,负数为补码存储(符号位不参与取反)。原因:为了让符号位能参与运算(有兴趣可以多了解,忘的差不多了)。
1.3 原子类
原子类的出现,就是为了解决java中的并发问题。其底层是采用硬件进行控制(cas+自旋),保证不会出现并发修改问题。常见的如AtomicBoolean,AtomicInteger,AtomicLong。
原子类基本上都提供了进行赋值、取值的方法。
1.4 标签式用法
如果你没听过也正常,但是如果说goto,那你就知道了。goto就是类似一个标签。
目前已经不提倡(或者说禁止)使用标签了,但是在jdk源码里面还可以找到,可以看出这帮爷们对自己代码的自信。
1.5 钩子函数
钩子函数是指在特定条件下会调用的方法,类似于try catch中的finally,以及object内的finalize方法。
1.6 volatile关键字
最早认识这个关键字是当年进行嵌入式c开发时认识的。
在java中作用也是类似的:一旦变量被volatile关键字修饰,那么jvm在对这个关键字不再进行缓存(硬件缓存,不同于软件缓存),每次读写均直接从系统底层获取,而不再由当前线程保存的值来确定。
这个关键字也可理解为专为并发而生。
1.7 ReentrantLock
一般的锁Lock,都是在对象加锁后就进行互斥的读写了。但是有一种情况,若持有锁的对象就是当前对象,那么此时采用Lock就不适用了。
重入锁,就是在进行对象加锁时,判断对该对象加锁的线程是否为当前线程,是的话就可以进行操作,不是的话根据公平锁与非公平锁有各自的处理逻辑。
1.8 自旋
自旋其实就是空循环,直到外部条件变化,才会跳出循环。
类似于这样:
for(;;;){
……
if(………){
break;
}
}
或者
while(true){
……
if(………){
break;
}
}
为什么会采用这种方式呢,因为这种操作一般执行时间很短,若跳出再执行的话,对系统资源消耗比较大。而采用自旋锁的话,当前线程一直占用cpu的执行时间,避免进行上下文的切换,反而提高了效率。
但是不建议这样写,首先第一个是不容易看懂,一旦关联代码有变化,这个地方就可能成为一个坑点;其次,现在的很多代码检测工具,也会认为这种代码是有问题的,可能都无法提交。
2、类图分析
ThreadPoolExecutor的类结构比较简单,继承自抽象类AbstractExecutorService,在最上层的Executor接口中,只有一个方法:
void execute(Runnable command);
所有ThreadPoolExecutor中要执行的线程,须实现了Runnable接口,才可加入线程队列。Runnable接口也简单,就一个方法:
public abstract void run();
也就是说,所有线程,最终都会执行这个run方法。
3、常量分析
ThreadPoolExecutor的常量分析,须和几个方法一起分析,才能看出设计的妙处。
3.1 关键常量与成员变量分析
先看定义几个比较关键的常量,理解了这里的东西,这个类就差不多读懂一半了。
3.1.1 常量
线程池的状态及说明,一定要同线程的状态区分开。
private static final int COUNT_BITS = Integer.SIZE - 3;
//0001 1111 1111 1111 1111 1111 1111 1111
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
//1110 0000 0000 0000 0000 0000 0000 0000
//能接受新任务,队列中的任务可继续运行
private static final int RUNNING = -1 << COUNT_BITS;
//0000 0000 0000 0000 0000 0000 0000 0000
//不再接受新任务,队列中的任务仍可继续执行
private static final int SHUTDOWN = 0 << COUNT_BITS;
//0010 0000 0000 0000 0000 0000 0000 0000
//不再接受新任务,不再执行队列中的任务,中断所有执行中的任务(发中断消息)
private static final int STOP = 1 << COUNT_BITS;
//0100 0000 0000 0000 0000 0000 0000 0000
//所有任务均已终止,workerCount的值为0,转到TIDYING状态的线程即将要执行terminated()钩子方法.
private static final int TIDYING = 2 << COUNT_BITS;
//0110 0000 0000 0000 0000 0000 0000 0000
//terminated()方法执行结束
private static final int TERMINATED = 3 << COUNT_BITS;
我们先看5个状态,只看最高的3位,分别是:
RUNNING = 111
SHUTDOWN = 000
STOP = 001
TIDYING = 010
TERMINATED = 011
目前看到这,我们要记住的一点就是:高3位用来进行表示线程池的运行状态。至于剩余的29位,我们下面还有分析。但是对于这几种状态的关系,我们来看看源码中的注释。
3.1.2 线程池状态转换
原文是英文,我也是看的迷迷糊糊的,就不献丑了,结合网上找的帖子,解释如下:
各状态之间可能的转变有以下几种:
RUNNING -> SHUTDOWN
调用了shutdown方法,线程池实现了finalize方法。
在finalize内调用了shutdown方法。
因此shutdown可能是在finalize中被隐式调用的
(RUNNING or SHUTDOWN) -> STOP
调用了shutdownNow方法
SHUTDOWN -> TIDYING
当队列和线程池均为空的时候
STOP -> TIDYING
当线程池为空的时候
TIDYING -> TERMINATED
terminated()钩子方法调用完毕
3.1.3 关键成员变量与私有方法
看源码:
//初始化线程数
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
//获取当前线程的运行状态
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; }
看到这,我们分析一下获取当前线程的运行状态与线程数方法:
//获取当前线程的运行状态
//~CAPACITY = 1110 0000 0000 0000 0000 0000 0000 0000
private static int runStateOf(int c) { return c & ~CAPACITY; }
取任意一个运行状态可知,与容量的取反进行与操作后,低29位全部屏蔽了,因此此时返回的必然是运行状态。
同理:
//获取当前的线程数
//CAPACITY = 0001 1111 1111 1111 1111 1111 1111 1111
private static int workerCountOf(int c) { return c & CAPACITY; }
这个时候进行与操作,屏蔽了高3位,只有低29位参与运算,可以快速的得出当前线程池中的线程数。
3.1.4 点评
将运行状态设置为小于0的数,便于判断当前线程池是否处于running状态;
仅使用32位存储线程池状态与线程池内的线程数,若需要获取线程池信息,只需要一个int即可,可见jdk底层源码的精简;
采用位运算,提高了执行效率;
同时,线程池状态还有扩增空间(2^3=8,目前只有5种状态),而线程池最大容量2^29,也可保证在绝大部分应用中是不会溢出的。而在源码中也声明了:如果在未来这个也成为一个问题,那么可以扩增为AtomicLong。
3.2 其他常量与成员变量
分析几个重要的
//待执行线程队列
private final BlockingQueue<Runnable> workQueue;
//锁,基于重入锁,线程池核心之一
private final ReentrantLock mainLock = new ReentrantLock();
//线程队列,这是该线程池内已有线程
//注意与workQueue的区别
private final HashSet<Worker> workers = new HashSet<Worker>();
//多线程协调通信
private final Condition termination = mainLock.newCondition();
//拒绝handler,用于线程池不接受新加线程时的处理方式
//分为系统拒绝(线程池要关闭等),与线程池饱和(已达线程池最大容量)
private volatile RejectedExecutionHandler handler;
//线程工厂,新建线程池时带入
private volatile ThreadFactory threadFactory;
//默认拒绝向线程池中新加线程的方式:丢弃
private static final RejectedExecutionHandler defaultHandler =new AbortPolicy();
4、常用方法分析
看完了常量,我们来开始进入正题吧。
4.1 execute
前面的类图也可以看到,向线程池中新增task时,会调用该方法。我们先看源码:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
//获取当前的线程池信息,状态+线程数量
int c = ctl.get();
//若当前线程数量未达corePoolSize
//corePoolSize可以理解为最小容量
if (workerCountOf(c) < corePoolSize) {
//向线程池中将本thread加进去
//若加成功,则直接返回
//未加成功的原因,下面还有源码分析
if (addWorker(command, true))
return;
//这里就是未加成功,再次获取当前线程池信息
c = ctl.get();
}
//再次检测当前线程池的运行状态
//并将当前线程加入到等待队列内
if (isRunning(c) && workQueue.offer(command)) {
//又获取一次线程池信息,跪下了
int recheck = ctl.get();
//检查当前线程池状态是否不在Running状态了
//若是,将线程cmd从等待队列内移除
//这个时候存在一种case,线程池不处于running状态
//但是remove失败了,这个时候看具体的queue处理了
//线程池还是很忠实的去尝试interrupt
if (! isRunning(recheck) && remove(command))
//线程池非running状态,并且移除cmd成功
//那么按处理策略拒绝cmd加入到线程池内
reject(command);
//检查一下当前线程池的缓存队列数量是否为0
else if (workerCountOf(recheck) == 0)
//根据情况是否要将新起一个线程
addWorker(null, false);
}
//若新起一个线程失败
else if (!addWorker(command, false))
//拒绝当前cmd
reject(command);
}
从源码可以看出,为避免数据过时,每次在进行操作前,都重新获取一次当前线程的信息。由于这是对线程池的操作,一般场景下,也不会出现很大量的并发向线程池新增线程的操作(如果有,首先得分析你的业务场景是否有必要这么做,如果必要且现有代码不满足你的需求,那么你可以覆写其中的部分代码)。
4.2 addWorker
看了上面的execute方法,我们再看addwork方法。这个方法是向线程池尝试新增一个线程,并执行firstTask。
//尝试向线程池内新增一个线程
private boolean addWorker(Runnable firstTask, boolean core) {
//注意标签
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
//若线程池处于非运行状态
//且
//或rs不处于SHUTDOWN状态(STOP、TIDYING、TERMINATED 之一)
//或firstTask不为空
//或缓冲队列为空
//那么返回false,表明新增一个线程失败(执行firstTask 也失败)
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
//此时线程池处于running状态,firstTask不为空
//且缓冲队列不为空,此时需要新增一个线程
for (;;) {
//获取线程池当前线程数量
int wc = workerCountOf(c);
//若线程池超过最大容量,或大于设定的容量
//corePoolSize与maximumPoolSize均为传入的参数
//那么直接返回false
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
//线程池未过限,那么采用cas机制,将线程池计数器扩增1,跳出标签
if (compareAndIncrementWorkerCount(c))
break retry;
//获取当前线程池信息
c = ctl.get(); // Re-read ctl
//若线程池状态有变更,从标签处重新循环
if (runStateOf(c) != rs)
continue retry;
//若线程池状态未变化,继续内层的for循环
}
}
//上面若将线程池计数器加1了
//这里就要对线程池扩增了
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
//创建一个线程实例
w = new Worker(firstTask);
//获取线程
//不在创建实例时直接run该线程,是避免构造函数未执行完,就run导致的异常
final Thread t = w.thread;
if (t != null) {
//重入锁
final ReentrantLock mainLock = this.mainLock;
//锁上,走起
mainLock.lock();
try {
//获取线程池状态
int rs = runStateOf(ctl.get());
//若线程池状态为running状态
//或为SHUTDOWN且传入的线程为空
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) {
//加入成功
//启动当前线程,将当前线程交有os管理
t.start();
//设置标志位
workerStarted = true;
}
}
} finally {
//若未启动成功
if (! workerStarted)
//回滚当前新起线程操作
//移除当前新增失败的线程
//将线程池计数器减1
//尝试中断线程池或者中断当前线程
addWorkerFailed(w);
}
//返回标志位,是否新增线程成功
return workerStarted;
}
感觉这段代码还没有讲清楚,后续有空可以再看看。
4.3 tryTerminate
还记得execute方法内的remove方法不,看源码:
public boolean remove(Runnable task) {
boolean removed = workQueue.remove(task);
tryTerminate(); // In case SHUTDOWN and now empty
return removed;
}
看其中的核心代码段tryTerminate:
//方法作用:进行状态转换以及中断一个线程
//当池的状态为SHUTDOWN且任务队列为空,需要将池的状态转变为TERMINATED;
//当池的状态为STOP且池中的当前活动线程数为0,要将池的状态转换成TERMINATED
//以及当线程池内的线程不为空时,中断第一个线程
final void tryTerminate() {
for (;;) {
int c = ctl.get();
//若
//或线程池处于运行状态
//或线程池已处于TIDYING/TERMINATED
//或:线程池处于SHUTDOWN且缓冲队列不为空
//返回,此时线程池状态不用做任何变更
if (isRunning(c) ||
runStateAtLeast(c, TIDYING) ||
(runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty()))
return;
//排除线程池状态不需要变更的状态,下面开始改变线程池状态
//若线程池内线程数量不为0
if (workerCountOf(c) != 0) { // Eligible to terminate
//给线程池的首个线程发中断消息
//进行尝试中断其运行
interruptIdleWorkers(ONLY_ONE);
return;
}
//获得全局锁
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
//这个地方的ctlOf作用是什么,没明白
//设置线程池的状态为TIDYING
if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
try {
//这个方法为留给ThreadPoolExecuor子类使用,为空代码
terminated();
} finally {
//将线程状态设置为TERMINATED
ctl.set(ctlOf(TERMINATED, 0));
//给其他线程发消息,告知该线程池即将关闭
//与termination对应的还有一个源码分析:awaitTermination
termination.signalAll();
}
return;
}
} finally {
mainLock.unlock();
}
}
}
4.4 run && runWorker
在execute我们介绍了,线程池内新增线程后,都是要调用run方法来进行执行的。我们看run源码:
public void run() {
runWorker(this);
}
再看runwork方法:
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
//循环
//从缓冲队列内拿出task,直到task内为空
while (task != null || (task = getTask()) != null) {
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条件写的真心难读,不能格式化一下吗
//实际是这样的:
//!wt.isInterrupted()
// && (runStateAtLeast(ctl.get(), STOP)
// || (Thread.interrupted()
// && runStateAtLeast(ctl.get(), STOP)))
//当前线程状态为非中断
//且
//或 线程池状态为STOP、TIDYING、TERMINATED
//或 当前线程状态为interrupted,且线程池状态为STOP、TIDYING、TERMINATED
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
//终端当前线程
//jdk源码注释写的很清楚
//这个地方发终端消息是与shutdownnow进行竞争
//因为这个地方中断以后,状态就变为stop及以上状态了
//发中断后就会不能再往线程池中新增任务
//但是可以继续执行线程池内的任务
wt.interrupt();
try {
//钩子函数,继承本类的子类可以进行一些操作
//在本类内为空方法
beforeExecute(wt, task);
Throwable thrown = null;
try {
//直接在当前线程内运行task
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 {
//同beforeExecute,也是钩子函数
afterExecute(task, thrown);
}
} finally {
//每次执行完,将completedTasks自增
//解锁
task = null;
w.completedTasks++;
w.unlock();
}
}
//标志位,能走到这,就说明线程执行没有异常中止
completedAbruptly = false;
} finally {
//当前线程执行完毕,进行收尾工作
processWorkerExit(w, completedAbruptly);
}
}
4.5 processWorkerExit
看完执行线程内的任务,我们先看线程退出清理方法processWorkerExit:
private void processWorkerExit(Worker w, boolean completedAbruptly) {
//为true则表明是该线程意外中断的
//将线程池的线程数减1
if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted
decrementWorkerCount();
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
//扩大统计
completedTaskCount += w.completedTasks;
//线程池内移除该线程
workers.remove(w);
} finally {
mainLock.unlock();
}
//改变线程池的状态
//或者中断第一个线程
tryTerminate();
int c = ctl.get();
//若线程池的状态处于STOP、TIDYING、TERMINATED
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
}
//这个时候有2种情况
//1、线程意外退出,尝试新增一个线程
//2、线程非意外退出,但是线程池还有线程,根据线程池内的情况决定是否要新增线程
addWorker(null, false);
}
}
4.6 getTask
在runworker内还有一个getTask方法,这个方法是从线程池内获取一个task,成功返回一个Runnable的task,失败返回null。
//从线程内轮询拿一个task出来
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.
//线程池状态为非运行状态
//且
//或线程池处于STOP、TIDYING、TERMINATED
//或线程池已空
//这个时候将线程池的的计数减1
//由于线程池已空了,返回空
//这个rs >= SHUTDOWN不懂,看起来作用不大??????????
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
int wc = workerCountOf(c);
// Are workers subject to culling?
//标志位
//或 线程池允许核心线程超时
//或 当前线程数大于最小线程数
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
//当前线程池中线程数大于最大线程数 或 timed 且 timedOut均为true
//且
//当前线程数大于1,或 线程池中可用线程为空
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
//若线程池中线程数成功减1
//返回空
//否则继续下一轮自旋
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try {
//根据标志位从缓冲队列内轮询或者直接拿一个task
//拿到了则成功返回
///没拿到或捕获到中断异常,则置超时标志位为true
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
4.7 小结
总的来说,这个线程池的源码我是半知半解的,对于线程池内的竞态条件还是没有完全理解。后续有新的理解,可以继续更新。
5、jdk实例源码分析
看完源码,没有一点实例,总是感觉很枯燥。还好,jdk源码自带了几个使用这个类的绝佳例子。
5.1 Executors
Executors是日常进行多线程开发使用较多的一个类,用的比较多的是其中2种:newFixedThreadPool、newCachedThreadPool.
5.1.1 newFixedThreadPool
这个比较好理解,固定大小的线程池。经过重载,有几种形式,我们来看其中一个源码:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads,
nThreads,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
这个其实也很好理解,新起一个线程池:
线程池最小大小和最大大小保持一致,若该线程没有可供执行的任务,那么即刻关闭该线程,
当有新task加入时没有空闲线程,那么将该线程加入到无界阻塞队列中,等待执行;
5.1.2 newCachedThreadPool
这个方法是新起一个线程池,允许线程池内的线程在空闲一定时间后,若还没有新任务加入,才关闭该线程,而不是直接关闭该线程。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
这个的意思是:新起一个线程池,特点:
最小线程数为0,最大为近乎无限,在线程池内该线程持续60s没有任务执行时,才关闭该线程,当有任务时,直接新起一个线程进行执行,若没有线程,则执行默认的处理策略进行处理(默认直接丢弃)。
5.1.3 简单对比
newFixedThreadPool由于有一个近乎无限的缓冲队列,那么任意多的请求过来,都可以放到缓冲队列中等待处理,但是由于处理的线程数固定,对于突发性的业务爆发,无法进行应对(若平时开启的很大线程数,浪费系统资源),可能会导致系统反应缓慢;
newCachedThreadPool由于在理论上只要有请求,都可以即时开启一个新线程进行响应,因而相应相对较快;但是一个系统内的最大线程数是有限的,一旦超过系统最大的线程数,那么多余的请求都会被直接抛弃掉(这种情况很难排查),这个时候可能反而起反作用。若同时开启的线程数较多,且jvm参数设置不合理(如内存分配较小,gc参数设置不合理),那么可能会导致系统发生频繁的gc,或者直接导致oom事件发生。
5.2 ScheduledThreadPoolExecutor
这个只是简介。这个类是继承的ThreadPoolExecutor,并且实现了ScheduledExecutorService接口,简单来说,这个类可以进行一些周期性的线程调度工作。
那么这个时候肯定可以联想到日常业务中的定时器。的确,不过用这个做定时器,现在很少了(有了spring-quartz)。
6、小结
由于之前对这个关注不多,只是想了解一下,这篇文章前后花了一周的业余时间才完成。但是个人感觉只是略知一二,还没有完全掌握其中的核心,若你有好的理解,可以回复我。
jdk源码分析,到这差不多告一段落了。后续还有其他的一些分享分析,欢迎继续留意。