返回

探秘 ReentrantReadWriteLock:深入浅出话写锁,揭开 AQS 加锁内幕

后端

ReentrantReadWriteLock 写锁加锁剖析

模版方法:写锁加锁套路

ReentrantReadWriteLock 的写锁如何加锁?它采用了模版方法模式,将加锁逻辑分解成多个步骤,然后通过不同的子类来实现这些步骤,从而实现不同的加锁策略。写锁的加锁模版方法如下:

public void acquire(int acquires) {
    if (!tryAcquire(acquires)) {
        doAcquireShared(acquires);
        if (hasQueuedPredecessors()) {
            doAcquireQueued(acquires);
        }
    }
}

tryAcquire:尝试获取锁

tryAcquire 方法尝试获取锁,如果成功则返回 true,否则返回 false。对于写锁来说,如果当前没有其他线程持有锁,则可以成功获取锁。否则,只能等待其他线程释放锁。

protected boolean tryAcquire(int acquires) {
    return state == 0 && compareAndSetState(0, acquires);
}

doAcquireShared:获取共享锁

doAcquireShared 方法尝试获取共享锁,如果成功则返回 true,否则返回 false。对于写锁来说,共享锁就是读锁,因此 doAcquireShared 方法实际上就是尝试获取读锁。

private void doAcquireShared(int acquires) {
    if (tryAcquireShared(acquires) < 0)
        doAcquireSharedInterruptibly(acquires);
}

doAcquireQueued:进入队列等待

doAcquireQueued 方法将当前线程加入等待队列,然后等待其他线程释放锁。当锁被释放时,当前线程将被唤醒并继续执行。

private void doAcquireQueued(int acquires) {
    boolean interrupted = false;
    try {
        for (;;) {
            final Node node = addWaiter(Node.EXCLUSIVE);
            for (;;) {
                final Node p = node.predecessor();
                if (p == null) {
                    int r = acquireQueued(node, acquires);
                    if (r == -1) {
                        if (interrupted)
                            throw new InterruptedException();
                        continue;
                    }
                    return;
                }
                if (p.nextWaiter == null) // fairness check
                {
                    interrupted = Thread.interrupted();
                    break;
                }
            }
        }
    } catch (InterruptedException ex) {
        cancelAcquire(node);
        throw ex;
    }
}

总结

ReentrantReadWriteLock 的写锁加锁逻辑相对复杂,但它背后的原理并不难理解。通过将加锁逻辑分解成多个步骤,并通过不同的子类来实现这些步骤,ReentrantReadWriteLock 实现了灵活的加锁策略。这种设计思想也体现了模版方法模式的精髓:将不变的部分抽象成模版方法,而将可变的部分留给子类来实现。

常见问题解答

  • Q:tryAcquire 方法失败的原因是什么?
    • A: 另一个线程已经持有写锁或读锁。
  • Q:doAcquireShared 方法失败的原因是什么?
    • A: 写锁已经被另一个线程持有。
  • Q:doAcquireQueued 方法做了什么?
    • A: 将当前线程加入等待队列并等待写锁释放。
  • Q:公平锁是如何实现的?
    • A: doAcquireQueued 方法会检查当前线程的前一个节点是否仍然存在,如果不存在,则当前线程将被唤醒。
  • Q:ReentrantReadWriteLock 如何防止死锁?
    • A: ReentrantReadWriteLock 通过不允许写锁降级为读锁,以及在写锁降级为写锁之前释放所有读锁来防止死锁。