另一把锁
除了前面说的 mainLock 外,线程池里面其实还有一把经常被大家忽略的锁。
那就是 Worker 对象。
可以看到 Worker 是继承自 AQS 对象的,它的很多方法也是和锁相关的。
同时它也实现了 Runnable 方法,所以说到底它就是一个被封装起来的线程,用来运行提交到线程池里面的任务,当没有任务的时候就去队列里面 take 或者 poll 等着,命不好的就被回收了。
我们还是看一下它加锁的地方,就在很关键的 runWorker 方法里面:
java.util.concurrent.ThreadPoolExecutor#runWorker
那么问题就来了:
这里是线程池里面的线程,正在执行提交的任务的逻辑的地方,为什么需要加锁呢?
这里为什么又自己搞了一个锁,而不用已有的 ReentrantLock ,即 mainLock 呢?
答案还是写在注释里面:
我知道你看着这么大一段英文瞬间就没有了兴趣。
但是别慌,我带你细嚼慢咽。
第一句话就开门见山的说了:
Class Worker mainly maintains interrupt control state for threads running tasks.
worker 类存在的主要意义就是为了维护线程的中断状态。
维护的线程也不是一般的线程,是 running tasks 的线程,也就是正在运行的线程。
怎么理解这个“维护线程的中断状态”呢?
你去看 Worker 类的 lock 和 tryLock 方法,都各自只有一个地方调用。
lock 方法我们前面说了,在 runWorker 方法里面调用了。
在 tryLock 方法是在这里调用的:
这个方法也是我们的老朋友了,前面刚刚才讲过,是用来中断线程的。
中断的是什么类型的线程呢?
就是正在等待任务的线程,即在这里等着的线程:
java.util.concurrent.ThreadPoolExecutor#getTask
换句话说:正在执行任务的线程是不应该被中断的。
那线程池怎么知道那哪任务是正在执行中的,不应该被中断呢?
我们看一下判断条件:
关键的条件其实就是 w.tryLock() 方法。
所以看一下 tryLock 方法里面的核心逻辑是怎么样的:
核心逻辑就是一个 CAS 操作,把某个状态从 0 更新为 1,如果成功了,就是 tryLock 成功。
“0”、“1” 分别是什么玩意呢?
注释,答案还是在注释里面:
所以,tryLock 中的核心逻辑compareAndSetState(0, 1)
,就是一个上锁的操作。
如果 tryLock 失败了,会是什么原因呢?
肯定是此时的状态已经是 1 了。
那么状态什么时候变成 1 呢?
一个时机就是执行 lock 方法的时候,它也会调用 tryAcquire 方法。
那 lock 是在什么时候上锁的呢?
runWorker 方法里面,获取到 task,准备执行的时候。
也就是说状态为 1 的 worker 肯定就是正在执行任务的线程,不可以被中断。
另外,状态的初始值被设置为 -1。
我们可以写个简单的代码,验证一下上面的三个状态:
首先我们定义一个线程池,然后调用 prestartAllCoreThreads 方法把所有线程都预热起来,让它们处于等待接收任务的状态。
你说这个时候,三个 worker 的状态分别是什么?
那必须得是 0 ,未上锁的状态。
当然了,你也有可能看到这样的局面:
-1 是从哪里来的呢?
别慌,我等下给你讲,我们先看看 1 在哪呢?
按照之前的分析,我们只需要往线程池里面提交一个任务即可:
这个时候,假如我们调用 shutdown 呢,会发什么?
当然是中断空闲的线程了。
那正在执行任务的这个线程怎么办呢?
因为是个 while 循环,等到任务执行完成后,会再次调用 getTask 方法:
getTask 方法里面会先判断线程池状态,这个时候就能感知到线程池关闭了,返回 null,这个 worker 也就默默的退出了。