返回
探秘 ReentrantReadWriteLock:深入浅出话写锁,揭开 AQS 加锁内幕
后端
2023-11-28 03:47:13
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 通过不允许写锁降级为读锁,以及在写锁降级为写锁之前释放所有读锁来防止死锁。