上一篇我们详细分析了AQS的原理和独占式获取的方式,现在先来回顾一下AQS的基本思想:
1.操作与规则分离:AQS实现了了同步状态的管理,线程的排队,等待与唤醒等底层操作,而把线程能否获取资源,如何获取资源等业务规则则交由子类实现
2.使用CLH队列来管理等待获取同步状态的线程。
AQS定义两种资源访问方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。本篇将讲解AQS对共享锁的实现!
一:AQS共享式获取思想
(1)多线程同时获取锁
共享式与独占式获取的最主要区别在于,共享式获取支持多个线程同时拥有锁,而独占式获取在任意时刻,最多只有一个线程获取到锁。比如当一个程序经常进行读取操作,而写操作确很少时,那么就需要对读线程使用共享锁,保证最大的读取量,而对写线程使用独占锁。
(2)顺序共享
考虑这样一个问题:假设总共有4个资源,在同步队列中有三个线程共享获取同步状态,如下图所示,
A使用2个,B需要3个,C需要1个,那么C会比B先抢夺同步状态吗?
答案是:同步队列中的线程会严格按照FIFO的顺序进行获取共享锁,只有等B获取锁之后,C才能获取共享锁。
二:执行流程
锁的获取过程:
1.先使用tryAcquireShared尝试获取共享锁,如果获取成功直接返回,进入临界区。
2.获取失败,将当前线程包装成共享类型节点加入同步队列,然后自旋,等待获取共享锁。
3.如果在队列中获取锁成功,就去唤醒后面还在等待获取共享锁的共享节点,直到遇到独占节点,或者资源不足。
锁释放过程:
1.当线程调用releaseShared()进行锁资源释放时,如果释放成功,则唤醒队列中等待的节点,如果有的话。
三:源码分析
锁的获取
通过上面对锁获取流程的介绍,下面就通过源码分深入的分析锁的获取过程。
首先看一下acquireShared方法:
public final void acquireShared(int arg) {
//1.使用子类实现的tryAcquireShared方法尝试获取共享锁
if (tryAcquireShared(arg) < 0)
//2.共享锁获取失败,就将当前线程加入同步队列,等待获取共享锁。
doAcquireShared(arg);
}
其中tryAcquireShared是由子类实现的,用来实现获取共享锁。
值得注意的是:该方法的返回值如果大于0表示当前线程获取锁成功,并且还有同步资源剩余,需要唤醒后继的节点来尝试获取共享锁。如果等于0表示当前线程获取共享锁成功,但它后续的线程是无法继续获取的,也就是不需要把它后面等待的节点唤醒。如果小于0表示当前线程获取锁失败。
下面来看一下doAcquireShared方法:
private void doAcquireShared(int arg) {
//1.将当前线程包装成共享节点,加入到同步队列中
final Node node = addWaiter(Node.SHARED);
//2.失败标记
boolean failed = true;
try {
//3.中断标记
boolean interrupted = false;
for (;;) {
//4.自旋
final Node p = node.predecessor();
//5.如果当前节点的前驱是头节点,使用tryAcqureShared方法尝试获取共享锁。
if (p == head) {
int r = tryAcquireShared(arg);
//6.如果tryAcqureShared返回值>=0,表示获取共享锁成功,使用setHeadAndPropagate设置当前节点为
//头节点,并唤醒后面的共享节点。这里就是共享的概念,也是和独占获取方式最大的区别。
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
//7.如果获取失败,就设置并判断前驱节点的waitStatus状态,睡眠等待唤醒
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
//8。如果获取锁失败,就将当前节点标记为取消节点。
if (failed)
cancelAcquire(node);
}
}
下面看setHeadAndPropagate的源码:
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
//1.propagate>0 表示还有剩余资源,需要唤醒后继共享节点
//2.h.waitStatus<0 如果h.waitStatus = PROPAGATE,表示之前的某次调用暗示了资源有剩余,所以需要
//唤醒后继共享模式节点,由于PROPAGATE状态可能转化为SIGNAL状态,所以直接使用h.waitStatus < 0来判断
//如果现在的头节点的waitStatus<0,唤醒
//3.h==null,表示此节点变成头节点之前,同步队列为空,现在当前线程获得了资源,那么后面共享的节点也
//可能获得资源
//以上3种情况可以看出,非常的保守,可能导致多次不必要的唤醒。
if (propagate > 0 || h == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
这里详细说一下第二点: 如果h.waitStatus = PROPAGATE,表示之前的某次调用暗示了资源有剩余,所以需要唤醒后继共享模式节点,由于PROPAGATE状态可能转化为SIGNAL状态,所以直接使用h.waitStatus < 0来判断。如果现在的头节点的waitStatus<0,唤醒。
假设:资源总数为8,线程A使用了4个资源,同步队列中的线程B正在使用2个资源,线程C需要2个资源,且此时也获得资源成功,那么线程C执行tryAquireShared方法时,就会返回0,表示获取同步资源成功,但是没有多余的资源,不需要唤醒后继共享节点。如下图所示:
可以看到setHeadAndPropagate
方法的原则是宁滥勿缺,反正doReleaseShared
方法会继续后来的处理:
下面来看doReleaseShared方法的源码:
private void doReleaseShared() {
for (;;) {
Node h = head;
//1.如果头节点不为空,且头节点不等于尾节点,说明还有线程在同步队列中等待。
//需要注意的是,等待队列的头节点是已经获得了锁的线程,所以如果等待队列中只有一个节点,那就说明没
//有线程阻塞在这个等待队列上
if (h != null && h != tail) {
int ws = h.waitStatus;
//2.如果头节点的waitStatus==SIGNAL,需要唤醒后面的线程,可以看作后继节点处于阻塞状态。
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
//3.如果头节点的状态为0,说明后继节点还没有被阻塞,不需要立即唤醒
//把头节点的状态设置成PROPAGATE,下次调用setHeadAndPropagate的时候前任头节点的状态就会
//是PROPAGATE,就会继续调用doReleaseShared方法把唤醒“传播”下去
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
//4.如果头节点被修改了那么继续循环下去
if (h == head) // loop if head changed
break;
}
}
1.AQS等待队列节点的PROPAGATE状态代表唤醒的行为需要传播下去,当头节点的后继节点并未处于阻塞状态时(可能是刚调用addWaiter方法添加到队列中还未来得及阻塞),就给头节点设置这个标记,表示下次调用setHeadAndPropagate函数时会把这个唤醒行为传递下去。
2.设置PROPAGATE状态的意义主要在于,每次释放permit都会调用doReleaseShared函数,而该函数每次只唤醒等待队列的第一个等待节点。所以在本次归还的permit足够多的情况下,如果仅仅依靠释放锁之后的一次doReleaseShared函数调用,可能会导致明明有permit但是有些线程仍然阻塞的情况。所以在每个线程获取到permit之后,会根据剩余的permit来决定是否把唤醒传播下去。但不保证被唤醒的线程一定能获得permit。
3.共享模式下会导致很多次不必要的唤醒。
锁的释放
public final boolean releaseShared(int arg) {
//1.使用子类实现的tryReleaseShared方法尝试释放资源,如果释放成功,就调用doReleaseShared方法
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
可以看到,当重写的tryReleaseShared(arg)方法返回true,成功释放锁资源,进入doReleaseShared()唤醒等待的线程,这个方法上面已经分析过,这里不再赘述。
参考:https://blog.csdn.net/Q_AN1314/article/details/79895128