获取锁失败
书接上文Java并发编程之ReentrantLock源码(一) 当前线程尝试获取锁失败,然后创建了一个节点添加到了队尾,
然后方法继续执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
// AQS类
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
这个方法主体部分是一个死循环,然后内部分为两个if代码块。
尝试获取锁
第一个if代码块:获取当前节点的前驱节点,如果前驱节点是head节点,那就说明当前节点处在第二的位置上,也就是下一个有资格获取锁的节点,所以可以尝试去获取锁,如果成功了就把当前节点标记为头节点,然后返回。
尝试进行park阻塞
第二个if代码块:如果前驱节点不是head节点,也就是说前面还有没获取到锁的节点,当前节点就没有资格去获取锁了,或者前驱节点是head节点但是尝试获取锁失败了,就会来到第二个if代码块。这里分别查看两个条件的源码如下:
waitStatus
首先看下waitStatus属性,默认值是0
volatile int waitStatus;
它的取值有以下几种
static final int CANCELLED = 1; // 表示线程被取消了
static final int SIGNAL = -1; // 表示后继节点需要被唤醒
static final int CONDITION = -2; // 表示线程在condition上等待
static final int PROPAGATE = -3; // 表示下一个acquireShared应该无条件传播
是否应该park阻塞
我们再来看shouldParkAfterFailedAcquire方法
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
根据名字其实可以大概知道意思是在获取锁失败后是否应该park。我们看下源码再分析下:
- 首先获取前驱节点的
waitStatus值,如果是Node.SIGNAL表示前驱节点可以用来唤醒当前节点,那么当前节点就可以放心park了,直接返回true。 - 如果大于
0也就是值为Node.CANCELLED表示前驱节点被取消了,但是现在它还在队列中,那就需要把它给排除掉,方法就是继续往前找没有取消的节点,然后把当前节点的指针指向找到的节点,然后就返回false。 - 如果
waitStatus都不是以上两种情况,那么就尝试通过CAS把它修改成Node.SIGNAL,然后返回false。
我们再回到acquireQueued方法,如果shouldParkAfterFailedAcquire方法返回false,那么就会继续for循环进行重试,再次进入shouldParkAfterFailedAcquire方法时,ws>0肯定不满足了,所以要么直接返回true,要么修改waitStatus值之后返回false,然后继续for循环重试,第三次再进入shouldParkAfterFailedAcquire方法时就一定会直接返回true了。
综上:
shouldParkAfterFailedAcquire方法的目的就是找到一个waitStatus值为Node.SIGNAL的前驱节点,这样当前线程就能放心park了。
进行阻塞
下面继续看parkAndCheckInterrupt方法
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
这个方法很简单,既然前面已经找到一个前驱节点可以用来唤醒当前线程了,那就放心调用park方法进行阻塞了。
但这里需要注意的是线程继续向下执行有两种情况:
- 被前驱节点唤醒
被前驱节点唤醒说明前驱节点释放了锁,当前线程被唤醒后返回
false,继续执行上面的for循环,重新尝试获取锁就成功了。
- 调用
interrupt方法进行中断
调用
interrupt方法中断当前线程,这里调用Thread.interrupted()方法返回true,但同时清除了中断标志位,同样地继续执行for循环进行重试,但因为是被中断重试的,前面可能还有其他节点或者锁还没有释放,所以很可能还是获取锁失败,那么就会继续进行park,如此循环往复。
这里单独解释一下Thread.interrupted(),我们知道如果park的线程被中断后继续调用park是不起作用的,所以调用这个方法清除了中断标志位,目的就是为了能够重新让线程park阻塞。
但是如果我们进行中断是想执行其他业务,但是中断标志位在这里被清除了怎么办?所以这个方法返回true之后用一个变量来记录是否被中断过,interrupted = true,但是仅仅用变量来记录还是不能恢复中断标志位,所以acquireQueued方法最终把interrupted返回到上一级,如果是true就会调用selfInterrupt方法
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
可以看到这里只是调用了一下interrupt方法,作用就是重新把中断标志位修改成true,不影响我们后续相关操作。不得不说Doug Lea真的是既严谨又巧妙。