==> 学习汇总(持续更新)
==> 从零搭建后端基础设施系列(一)-- 背景介绍
摘要:上篇实现了简单的无锁线程池,中篇实现了简单的加锁线程池,本篇着重剖析java线程池源码。
一、基础概念
其实,把下面这些参数概念理解了,看线程池源码像看helloworld一样简单。
public void run(){
while(true){
//不停的执行任务或者等待执行任务
}
}
LinkedBlockingDeque
的实现。public E takeFirst() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lock();
try {
E x;
//如果队列一直为空,那么就一直阻塞在那
//当队列不为空了,notEmpty.await()会被唤醒
while ( (x = unlinkFirst()) == null)
notEmpty.await();
return x;
} finally {
lock.unlock();
}
}
……
public E pollFirst(long timeout, TimeUnit unit)
throws InterruptedException {
long nanos = unit.toNanos(timeout);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
E x;
//这个和take不一样的是,可以指定等待时间,超时直接返回null
while ( (x = unlinkFirst()) == null) {
if (nanos <= 0)
return null;
nanos = notEmpty.awaitNanos(nanos);
}
return x;
} finally {
lock.unlock();
}
}
new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "我是从线程工厂出生的");
}
};
通过线程工厂,我们能对线程进行个性化的定制,再具体的就不演示了。 private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
可以看一下源码中的注释是如何解释这个缩写的参数的,它其实就是一个控制线程池大小和状态的参数。int COUNT_BITS = Integer.SIZE - 3;
int CAPACITY = (1 << COUNT_BITS) - 1;
int RUNNING = -1 << COUNT_BITS;
int SHUTDOWN = 0 << COUNT_BITS;
int STOP = 1 << COUNT_BITS;
int TIDYING = 2 << COUNT_BITS;
int TERMINATED = 3 << COUNT_BITS;
System.out.println("COUNT_BITS = " + COUNT_BITS);
System.out.println("CAPACITY = " + CAPACITY + " = 000" + Integer.toBinaryString(CAPACITY));
System.out.println("~CAPACITY = " + ~CAPACITY + " = " + Integer.toBinaryString(~CAPACITY));
System.out.println("RUNNING = " + RUNNING + " = " + Integer.toBinaryString(RUNNING));
System.out.println("SHUTDOWN = " + SHUTDOWN + " = " + Integer.toBinaryString(SHUTDOWN));
System.out.println("STOP = " + STOP + " = 00" + Integer.toBinaryString(STOP));
System.out.println("TIDYING = " + TIDYING + " = 0" + Integer.toBinaryString(TIDYING));
System.out.println("TERMINATED = " + TERMINATED + " = 0" + Integer.toBinaryString(TERMINATED));
ctl<0
那么线程池就是正常运行的,如果ctl>=0
就是要关闭了。for(;;);
,可能有人会问,为什么没见过用while
来自旋的?其实吧,都行,从现在来看,两者效率基本一样。cmpxchg
,其作用是比较并交换操作数(感兴趣的百度一下这个指令的具体释义)。java是不能直接使用汇编的,那怎么办呢?java可以通过C语言间接的去使用这个指令,那么就引出了Unsafe
这个类,简单的讲,这个类给你提供了类似C/C++的操作内存的方法,所以非常危险,我们如果要使用只能通过反射拿到它进行使用。AbstractQueuedSynchronizer
,它是一个队列同步器。它的作用就像是一条管道一样,严格的控制线程的先进先出,任一时刻,有且仅有一个线程能运行,其它都会被阻塞。比如说,有2个线程,同时争夺一个资源,那么线程1先拿到了,线程2再想去拿,就拿不到,并且放到队尾排队,只有线程1释放了这个资源,出队了,线程2才能拿到。依次类推。关于AQS后续通过其它文章再深入探讨。二、ThreadPoolExecutor源码剖析
通过上面对基础概念的解释,这里就不再重复对其构造函数分析了。
1.任务提交的三种方式
void execute(Runnable command)
Future submit(Runnable task)
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}
……
protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
return new FutureTask<T>(runnable, value);
}
……
public interface RunnableFuture<V> extends Runnable, Future<V> {
void run();
}
这里对task的包装挺巧妙的,因为接口可以多继承,所以包装一个新的接口RunnableFuture
继承自Runnable
和Future
,这样再通过中间人FutureTask
将Runnable
和Callable
包装起来,完美! Future submit(Callable task)
这三种提交方式,最后都由execute
执行。
2.线程池入口execute源码剖析
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
//判断当前线程池的大小是否小于核心线程数
if (workerCountOf(c) < corePoolSize) {
//如果还未达到核心线程数,那么就继续创建线程
if (addWorker(command, true))
return;
//这里尝试获取最新的线程数量
c = ctl.get();
}
//如果线程池大小已经达到核心线程数,那么就放到队列中排队
if (isRunning(c) && workQueue.offer(command)) {
//不管是多少次检查,都是为了尽可能感知并发下的状态变化
int recheck = ctl.get();
//如果线程池关闭了,那么就不要再往队列里面加任务了,这里
//尝试删除之前加入的那个任务,注意,这个操作是可能失败返回
//false的,因为你一加进去,可能就立马被执行了。如果删除成功
//就使用相应的拒绝策略处理
if (! isRunning(recheck) && remove(command))
reject(command);
//这个就比较耐人寻味,线程池正在运行并且大小为0的时候创建一个线程
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//这个就很好理解了,当核心线程满了,队列满了,就创建非核心线程
//来处理,如果非核心线程也满了,就执行相应的拒绝策略处理了
else if (!addWorker(command, false))
reject(command);
}
addWorker
还可能返回false?int c = ctl.get();
或者前后相隔非常非常短的时候进行int c = ctl.get();
,这导致的结果是什么呢?线程没创建出来,线程池大小没更新,大家拿到的都是一个值,都小于corePoolSize
,所以需要在addWorker
里面进行拦截判断(具体的下面再讲),如果已经有2个核心线程创建完了,那么剩下的2个就会返回false,继续进行下面的分支判断。workerCountOf(recheck) == 0
为true
?3.创建工作线程addWorker源码剖析
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get();
//拿到线程池运行状态
int rs = runStateOf(c);
//1.线程池状态大于等于SHUTDOWN
//2.!(线程池状态正好等于SHUTDOWN && firstTask == null && ! workQueue.isEmpty())
//同时满足第一个条件和第二个条件,就不用继续往下创建工作线程了
//通俗的解释这段代码的意思,第一个条件很容易明白,就是线程池要
//关闭了,不能创建工作线程了。第二个条件就得琢磨一下,就是线程
//池状态正好等于SHUTDOWN,并且task是空的,并且队列不为空,那
//肯定得再搞个线程去处理队列中剩余的任务吧。反之如果线程池状态大于
//SHUTDOWN,就是要立马关掉,不做任何后续处理,那当然就不需要
//再创建工作线程了。
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
//拿到当前线程池大小
int wc = workerCountOf(c);
//1.线程池大小超过理论最大容量
//2.如果要创建的是核心线程,那么要判断核心线程数是否已经满了,非核心线程同理
//只要满足1和2其中一个条件,就返回false,创建失败
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
//如果前面的关卡都过了,说明可以创建,然后提前增加线程池大小
//这里使用了CAS去增加
if (compareAndIncrementWorkerCount(c))
break retry;
//如果修改失败了,拿到最新的值
c = ctl.get(); // Re-read ctl
//判断一下线程池状态是否变化,如果有变化,就跳到外层循环
//返回继续在内层循环使用CAS尝试增加线程池大小
if (runStateOf(c) != rs)
continue retry;
}
}
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;
//下面这里开始加锁,因为涉及到HashSet的读写,它是线程非安全的
mainLock.lock();
try {
int rs = runStateOf(ctl.get());
//1.线程池正常运行
//2.线程池状态等于SHUTDOWN并且firstTask为空,意思就是创建一个线程去消耗队列里面的任务
//满足以上任意一个条件都能创建
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
//检查这个线程是否正在运行,因为还没进行到下面t.start(),在这里就运行了的话,明显是不合法的(具体什么场景会碰到,我也不清楚)
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;
}
for
循环?execute
源码剖析那里了,还记得上面分析过,如果并发提交任务,会在第一个if分支里,全部进入到addWorker
里,如果这时候不加锁,会出现创建了4个核心线程的情况,使用了CAS操作,能保证只有2个核心线程被创建,其它两个会返回false。addWorker
,也能让外面还在想进来的任务死了这条心。4.工作线程Worker源码剖析
private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{
private static final long serialVersionUID = 6138294804551838833L;
//工作线程
final Thread thread;
//创建Worker的时候,如果传了Task,那么就直接执行这个Task,否则去队列中拿Task
Runnable firstTask;
//这就是一个统计用的
volatile long completedTasks;
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
//这里创建的线程,传入的Runnable就是Worker本身
//当在外面调用t.start的时候,就会运行Woker中的run方法
this.thread = getThreadFactory().newThread(this);
}
public void run() {
//真正执行的方法
runWorker(this);
}
//state == 0表示目前没有被线程持有,state == 1表示目前被一个线程持有,相当于被锁住了
protected boolean isHeldExclusively() {
return getState() != 0;
}
//尝试加锁,如果当前state == 0,那么尝试将state置为1,也就是加锁
protected boolean tryAcquire(int unused) {
if (compareAndSetState(0, 1)) {
//设置当前线程为这个Worker的独占线程(就是排他锁)
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) {
}
}
}
}
setState(-1)
代码?5.执行任务runWorker源码剖析
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
//这是防止重复执行这个task
w.firstTask = null;
//这里就是将state位置0的操作
w.unlock(); // allow interrupts
//这个变量也有点意思,考虑得比较全面,这个是防止while循环里面有捕获不到的异常,也就是非正常结束。最后会有相应的处理策略。
boolean completedAbruptly = true;
try {
//task != null就先执行它,然后才去队列里面拿task
//getTask有三种可能,第一个是返回task,第二个是返回null,第三个是一直阻塞(具体的分析getTask源码再讨论)
while (task != null || (task = getTask()) != null) {
//执行任务的时候先加锁
w.lock();
//这种代码在线程池很常见,无非是判断当前线程池的状态,做相应的处理,可自己试着分析一下,很简单
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();
}
}
//运行到这里,说明没有未知异常,置为false
completedAbruptly = false;
} finally {
//这就是后续的处理,里面没啥好讨论的,感兴趣可自行分析
processWorkerExit(w, completedAbruptly);
}
}
//尝试中断空闲的线程
private void interruptIdleWorkers(boolean onlyOne) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
for (Worker w : workers) {
Thread t = w.thread;
//重点来了,这里中断之前,需要判断一下线程是否空闲,怎么判断呢?简单,我们尝试去获取锁,如果获取成功,说明那个线程没有在执行task.run,也就是空闲的了。
if (!t.isInterrupted() && w.tryLock()) {
try {
t.interrupt();
} catch (SecurityException ignore) {
} finally {
w.unlock();
}
}
if (onlyOne)
break;
}
} finally {
mainLock.unlock();
}
}
6.获取任务getTask源码剖析
代码分析
private Runnable getTask() {
//这个变量的设计,看一下中篇就知道了,很是巧妙
boolean timedOut = false;
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
//还是判断线程池状态那一套
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
int wc = workerCountOf(c);
//是否允许回收线程,很好理解,如果允许核心线程被回收或者当前线程池大小大于核心线程数,都是允许线程被回收的。
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
//这一段,判断当前线程是否可以回收,如果可以,先将线程池大小减1,并且返回null,外面的循环就会退出了,线程也就退出了
//再来看看具体的条件
//1.当前线程池大小大于最大线程数
//2.允许线程被回收 并且 超过指定时间没获取到任务 并且 (当前线程池中还有至少2个线程 或者 队列为空了)
//满足1和2任意一个条件就可以回收线程了
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
//这里也是用CAS尝试去将线程池大小减1
if (compareAndDecrementWorkerCount(c))
return null;
//如果失败了,说明有其它线程成功了,那就重复上述步骤
continue;
}
try {
//允许回收的话,就调用poll,它可以设置超时时间,如果指定时间内没有任务处理,timedOut就为true,说明可以回收了
//反之就调用take一直阻塞下去
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
Q&A
1)为什么在获取Runnable为null的时候,直接递减线程池大小,然后返回null呢?
如果你自己手撸过线程池,那么应该会遇到这个场景,也就知道为什么了,现在我举个小例子
try {
boolean timed;
try {
lock.lock();
//因为这里锁住了,N个线程都会顺序判断一遍,注意,这里会非常快
timed = currentPoolSize.get() > corePoolSize;
//if(timed && timeout) {
// currentPoolSize.decrementAndGet();
// return null;
//}
} finally {
lock.unlock();
}
//这里执行的会比上面的慢一些,会导致什么问题呢?
//最坏的情况下,N个线程都直接运行到了这里,并且timed=true
//那么会有很多runnable == null
Runnable runnable = timed ? workerQueues.poll(keepTimeAlive, TimeUnit.NANOSECONDS) : workerQueues.take();
if(runnable != null){
return runnable;
}
//这里就会递减过头了,刹不住车
currentPoolSize.decrementAndGet();
return null;
//timeout = true;
} catch (InterruptedException e) {
//timeout = false;
}
当然了,办法肯定有的,加锁,判断等等都行,麻烦且效率低下,那还不如递减先于获取来得实在呢。
2)InterruptedException作用是什么?
从名字就可以看出,中断异常,那么作用就很明显了,当调用的是take阻塞的时候,当线程中断的时候,这个就会抛异常。一般什么时候中断线程呢?关闭线程池的时候。
7.线程池shutdown源码剖析
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
//这就是更新线程池的状态
advanceRunState(SHUTDOWN);
//中断闲置的线程,还在执行的任务的让它继续收尾(上面已经分析过这个方法)
interruptIdleWorkers();
//开放给子类的方法
onShutdown(); // hook for ScheduledThreadPoolExecutor
} finally {
mainLock.unlock();
}
//尝试将线程池状态置为TERMINATED,并且还有一些相应的处理
tryTerminate();
}
private void advanceRunState(int targetState) {
for (;;) {
int c = ctl.get();
//很简单的条件,比如targetState == SHUTDOWN
//那么c >= SHUTDOWN的话,就不需要修改了
//如果线程池还在运行状态,那么CAS修改它的状态
if (runStateAtLeast(c, targetState) ||
ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c))))
break;
}
}
final void tryTerminate() {
for (;;) {
int c = ctl.get();
//1.对于shutdown这个方法来说,调用这个方法的时候,线程池状态不可能是running了
//2.也不可能是 >= TIDYING,应该是SHUTDOWN
//3.如果是SHUTDOWN,并且队列中还有任务,也不行
if (isRunning(c) ||
runStateAtLeast(c, TIDYING) ||
(runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty()))
return;
if (workerCountOf(c) != 0) { // Eligible to terminate
//这里我也不知道为什么只中断一个?
interruptIdleWorkers(ONLY_ONE);
return;
}
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
//这段代码就比较简单,就是线程池状态从TIDYING -> TERMINATED
//之间,可以给子类一个收尾处理的方法
if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
try {
terminated();
} finally {
ctl.set(ctlOf(TERMINATED, 0));
termination.signalAll();
}
return;
}
} finally {
mainLock.unlock();
}
// else retry on failed CAS
}
}
8.线程池shutdownNow源码剖析
public List<Runnable> shutdownNow() {
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
advanceRunState(STOP);
//和shutdown不同的是,不管是不是空闲线程,全部都中断了
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();
}
}
public static void main(String[] args) throws InterruptedException {
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(2, 2, 60, TimeUnit.SECONDS, new LinkedBlockingDeque<>());
for (int i = 0; i < 4; i++) {
poolExecutor.execute(() -> {
System.out.println("当前线程:" + Thread.currentThread().getName() + " 开始");
while (true){
if(Thread.currentThread().isInterrupted()){
System.out.println("接收到中断信号,退出");
break;
}
}
System.out.println("当前线程:" + Thread.currentThread().getName() + " 结束");
});
}
Thread.sleep(2000);
System.out.println("线程池关闭");
List<Runnable> runnables = poolExecutor.shutdownNow();
System.out.println(runnables.size());
}
三、总结
说实话,线程池的源码我之前看过很多次,但是每次都是懵懵懂懂,然后就是看不下去了,觉得太难了。然后突然兴起,想自己撸一个线程池,看看难度几何,写的过程,遇到很多问题,然后尝试去解决,最后发现有些思想竟然和java线程池一模一样,那些困扰我的问题也一一解开,那种感觉非常棒,哈哈。所以真就应了那一句,实践出真理啊,偷懒最终的苦还是自己承受。