一、引言
共享锁区别于独占锁,多个线程可以同时持有锁,java中的Semaphore、CountdownLatch、ReentrantReadWriteLock中的readerLock都是共享锁,独占锁的原理在juejin.cn/post/684490…中已经介绍过,建议读者先阅读这一部分。
二、共享锁的获取
共享锁的获取分响应中断和忽略中断两种,下面介绍响应中断共享锁获取。
2.1. 响应中断获取共享锁
以CountdownLatch为例,await()底层调用的是AQS.acquireSharedInterruptibly(),在响应中断的条件下获取共享锁。
//在获取到指定锁之前,线程阻塞。
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
AQS.acquireSharedInterruptibly(int arg):获取共享锁,当线程被中断时放弃。
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
//如果线程被中断,抛出InterruptedException,用于业务线程catch该异常,可用于是否停止线程
//的运行
if (Thread.interrupted())
throw new InterruptedException();
//获取共享锁,由子类实现
if (tryAcquireShared(arg) < 0)
//获取共享锁,当线程被中断时抛出InterruptedException,放弃锁的获取。
doAcquireSharedInterruptibly(arg);
}
tryAcquireShared()由子类实现,返回值:
- 大于0:当前线程获取锁成功,后续线程获取锁可能成功(由于竞争);
- 0:当前线程获取锁成功,后续线程获取锁失败;
- 小于0:当前线程获取锁失败。
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
//以获取共享锁的方式,将当前线程插入等待队列,该方法在独占锁的分析中已经讲解。
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
//获取当前节点的pre节点
final Node p = node.predecessor();
//只有pre是head,才能轮到当前节点获取锁。
if (p == head) {
//获取锁,由子类实现
int r = tryAcquireShared(arg);
if (r >= 0) {
//获取锁成功,重新设置head节点,并传播状态。
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
//竞争锁失败后,是否能挂起当前线程;
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()) //挂起该线程,当线程被唤醒后,返回中断标志位。
//如果线程被中断,抛出异常,终止该线程对锁的竞争。
throw new InterruptedException();
}
} finally {
if (failed)
//放弃竞争锁。
cancelAcquire(node);
}
}
AQS.setHeadAndPropagate(Node node, int propagate): 当前线程获取共享锁成功,需要重新设置head节点,并传播状态。
这里区别与独占锁,独占锁中获取锁成功后,只是设置head节点,只有当锁释放时才会唤醒后继节点;共享锁由于可以多个线程同时持有锁,因此在一个线程获取锁成功后就应该唤醒后继节点。
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // 记录当前的head节点
setHead(node); //由于当前线程已经成功获取锁,所以可以将node->head.
/*
*如果当前线程获取到锁(propagate>0),|| head=null || head.waitStatus<0,需要唤醒后继节点。
*/
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
//s为null说明当前已经没有其他锁尝试获取锁,s.isShared()说明当前是共享锁状态
//对于上面的任意一种情况满足时,执行doReleaseShared(),用于唤醒后继线程。
if (s == null || s.isShared())
doReleaseShared();
}
}
AQS.doReleaseShared:释放锁,并唤醒后继节点。
private void doReleaseShared() {
/*
* 共享锁下,可能同时有多个线程持有一把锁,意味着同时会有多个线程获取锁/释放锁。
* 确保锁的释放,即使此时可能有其他线程获取/释放锁。
*/
for (;;) {
Node h = head;
//h != null && h != tail 说明此时至少还有一个线程在等待锁。
if (h != null && h != tail) {
int ws = h.waitStatus;
//如果h.waitStatus=SIGNAL,唤醒其后继节点。
if (ws == Node.SIGNAL) {
//由于此时其他线程也在释放锁,都在执行doReleaseShared(),一个线程执行compareAndSetWaitStatus成功即可,并唤醒后继节点。
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
} //如果后继节点不需要唤醒,设置状态。
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // 如果在此期间,head节点变动了,继续循环,唤醒后继节点。
break;
}
}
2.2 忽略中断获取共享锁
以下是忽略中断获取共享锁的入口,与响应中断获取共享锁不同,前者对于中断的处理只是将中段位置位,后者则是通过抛出InterruptedException的形式推出锁的竞争。
AQS.doAcquireShared(int arg) :
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
//如果线程被中断,中断自己。
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
//线程竞争锁失败后,是否能够挂起
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()) //挂起线程并检查中断位
//如果线程被中断,设置interrupted
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
三、共享锁的释放
AQS.releaseShared(int arg):
public final boolean releaseShared(int arg) {
//释放线程,tryReleaseShared返回true表示后继线程可能可以成功获取锁,因此需要调用doReleaseShared(),唤醒后继线程;
//否则,没有必要唤醒后继线程,直接返回即可。
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
独占锁在释放锁时,当重入次数=0时,代表当前线程已经完全释放锁,此时会调用unparkSuccessor()方法,唤醒后继线程。而共享锁模式下,只有当后继线程可能获取锁成功时(由tryReleaseShared定义,在子类中实现),才唤醒后继线程。
至此AQS共享锁的原理分析完毕。
参考:segmentfault.com/a/119000001…