Java并发编程之ReentrantLock源码(二)

102 阅读4分钟

获取锁失败

书接上文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真的是既严谨又巧妙。