聊一聊FutureTask源码中体现的“自旋锁”思想
前言
这篇文章记录了笔者自己对FutureTask的部分源码设计的思考与心得,属于笔者自己的观点,若有哪位热爱源码研究的同仁觉得我说的不对,欢迎批评指正。
提示:在阅读之前必须对FutureTask的源码和实现原理有一定的了解。本文要聊的内容在FutureTask中awaitDone方法之中。
FutureTask中awaitDone方法的源码
在FutureTask源码中,awaitDone方法的作用是“Awaits completion or aborts on interrupt or timeout.”,即等待任务完成,或者在中断或超时的时候终止。其用于FutureTask的阻塞式获取任务执行结果的get()方法之中,如果在某线程调用FutureTask的get()方法获取任务执行结果,但是任务还没执行完成,则使用awaitDone方法将该线程阻塞。
因为可能不止一个线程去调用get()方法被阻塞,所以FutureTask将所有阻塞等待的线程封装成WaitNode节点,然后使用一个栈来存储这些WaitNode节点。
/*
* timed:是否定时等待,即等待是否有超时时间
* nanos:如果是定时等待,此参数是等待的时间
*/
private int awaitDone(boolean timed, long nanos)
throws InterruptedException {
//如果等待有超时时间,计算等待的截止时间
final long deadline = timed ? System.nanoTime() + nanos : 0L;
//用于封装等待线程的WaitNode节点
WaitNode q = null;
//节点是否入栈
boolean queued = false;
//开启循环
for (;;) {
//如果线程被中断
if (Thread.interrupted()) {
//从栈中移除当前线程的WaitNode节点,并且抛出中断异常
removeWaiter(q);
throw new InterruptedException();
}
//获取当前任务的状态
int s = state;
//任务已经结束了(正常完成、抛出异常、被取消)则返回state
if (s > COMPLETING) {
if (q != null)
q.thread = null;
return s;
}
//如果任务即将完成,让其他线程先执行
else if (s == COMPLETING) // cannot time out yet
Thread.yield();
//如果任务没有完成,也不处于即将完成的中将状态COMPLETING,则进入自旋
//第一次循环,如果当前WitNode为null,创建一个WaitNode结点
else if (q == null)
q = new WaitNode();
//第二次循环,如果当前WaitNode节点没有入队,则尝试入队
else if (!queued)
queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
q.next = waiters, q);
//第三次循环,根据是否有超时时间来挂起线程
else if (timed) {
//计算当前时间离超时时间点还有的时间差
nanos = deadline - System.nanoTime();
//超出了指定时间,就移除当前节点并返回任务状态
if (nanos <= 0L) {
removeWaiter(q);
return state;
}
//挂起线程(有超时时间)
LockSupport.parkNanos(this, nanos);
}
else
//挂起线程
LockSupport.park(this);
}
}
用流程图演示心中的疑问
下面,我将awaitDone方法的执行流程画图表示,再谈我先前对awaitDone方法执行流程的疑问。
在上面的流程图之中,有一点让我最初感觉很奇怪,那就是在执行“创建节点和”和“节点入栈”之后,为什么不接着走下面的流程,而是回到最上面的判断“线程是否中断”这一步骤?这样不是多次一举,去多执行两次无意义的循环吗?
我以前认为,正确的流程应该是这样的。如果判断未创建节点,则先创建节点,然后继续执行“判断节点是否入栈”后续步骤;如果判断节点未入栈,则先执行节点入栈,然后继续执行判断是否定时等待后续步骤。这样的设计似乎让程序更加高效。
下面画出我先前认为更加合理的流程图,红色方框内是变化的内容。
用自旋锁解释awaitDone方法的流程设计
自旋锁是什么
刚开始,我任务这是JDK源码的缺陷。后来我随着知识面的扩展,我了解到有一种锁叫做“自旋锁”。
先看自旋锁的概念解释:
自旋锁是为高效并发而产生的一种锁优化的思想,要实现线程互斥同步往往需要对某些线程进行阻塞,而线程的挂起和恢复都要转入到内核态去完成。作为Java程序员,笔者也不太懂什么是内核态,但是需要知道的是线程的挂起和恢复是一种重量级的操作,也就是比较消耗CPU性能。
如果共享数据的锁定时间很短,那么为了这很短的时间去进行挂起和恢复线程这个重量级操作就划不来。对于多核CPU来说,可以多个线程并行,让需要请求锁的线程“稍等一下”比直接阻塞要好得多,这样线程大概率可以在等待中获取其他线程释放的锁,就不用放弃CPU的执行时间。
于是产生了自旋锁,自旋锁就是为了让线程等待获取锁而让线程去执行一个有限的循环,这种循环又叫做线程的自旋。
自旋锁思想的体现
从自旋锁思想来看,awaitDone方法中多执行的两次循环并不是无意义的,也不是编码上的缺陷,而是刻意为之。
线程在进入阻塞之前的两次多余的循环,是为了等待任务执行结束而进行的自旋,也就是应用了自旋锁的思想。因为线程的阻塞和重启是比较消耗性能的重量级操作,所以JDK源码中尽量做到能不阻塞线程就不阻塞,故意让线程多进行了两次循环(自旋),为任务执行结束而“稍等一下”,如果两次自旋还等不来任务完成,再阻塞线程。
这也是为什么将FutureTask将任务状态细分为COMPLETING(即将结束)和NORMAL(正常结束)的原因,如果任务已经执行完成,但是还没有给返回结果赋值,就将任务状态设为COMPLETING,好让需要获取执行结果的线程知道任务已经快要结束了,可以使用Thread.yield()暂时让出CPU时间片,稍等片刻后就能直接获取到结果而不用阻塞。一切设计都体现了能不阻塞就不阻塞的思想。