上一篇博客介绍了AQS中的排他锁,现在说一下AQS中的共享锁。
共享锁的主要实现就是semaphore,countDownLatch。这两个类在应用中还是比较多的,尤其是countDownLatch,在控制主线程和子线程同步的时候,经常会看见。
源码解析
获取锁
public final void acquireShared(int arg) {
// 剩余锁的资源是否大于0
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
private void doAcquireShared(int arg) {
// 设置锁的模式为共享锁,接入到队尾,并且设置成tail
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 前一个node
final Node p = node.predecessor();
if (p == head) {
// 尝试获取锁
int r = tryAcquireShared(arg);
if (r >= 0) {
// 设置head并且传播
// 这边和排他锁有点不一样,排他锁设置成head就可以了,这边还要再传播
// 具体逻辑下面再分析
setHeadAndPropagate(node, r);
p.next = null; // help GC
// 判断中间是不是有打断过
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
// 和排他锁一致,把前面的节点设置成signal
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()) // park唤醒时候判断是被unpark唤醒还是interrupt
interrupted = true;
}
} finally {
if (failed)
// 如果异常,取消获取锁,锁的状态改成CANCELLED
cancelAcquire(node);
}
}
其实这边的逻辑只有setHeadAndPropagate不一致,其他都是一致的,这边主要来看下这个方法
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
// 既然当前线程拿到了锁,就把当前node设置为head
setHead(node);
// propagate是还剩余的资源
// h == null ,可能是其他线程在释放的时候,前一步p.next=null后垃圾回收了
// h.waitStatus<0,需要向后唤醒
// (h =head)==null, h指向了新创建的head,同样可能被释放后垃圾回收
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
// 向后节点是共享节点
// s == null, 我觉得还是可以像之前那样理解,node被指向了head,
// 然后被其他线程释放了p.next=null,所以next==null
if (s == null || s.isShared())
// 释放锁,并且向后唤醒
// 在release时候也会调用此方法
doReleaseShared();
}
}
因为共享锁可以多个线程持有锁,所以在获得锁的时候,也会尝试唤醒后面的共享锁的线程,源码中也说了,可能会存在无效唤醒。但是也没有什么问题,拿不到也没关系,就会再次阻塞,和排他锁的流程也是一致的。再来看下doReleaseShared方法。
private void doReleaseShared() {
for (;;) {
Node h = head;
// 如果head != tail
if (h != null && h != tail) {
int ws = h.waitStatus;
// 如果有后继节点,并且当前节点没有被改变
// 唤醒后继节点
// ①
if (ws == Node.SIGNAL) {
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
}
// ③
// head是否改变
if (h == head) // loop if head changed
break;
}
}
其实这边代码有一些点:
①:如果在原来有后继节点的时候,因为共享锁可能有多个,这段代码可能也会多个执行,保证只有一个在运行。
②:ws=0那说明这个节点是尾部节点,那既然在①的时候head节点不是SIGNAL了。说明在①的时候,这个尾部节点变成了head节点,此时ws=0,但是后面的cas却失败了,那就可能是再有一个线程加入了进来,在执行到后面compareAndSetWaitStatus时候有个新的node加进来把waitStatus设置成了SIGNAL,这时继续走获取head唤醒流程。
③:如果head没有变化,说明没有人更改head,整个流程执行结束。如果head变化,重新循环,唤醒后面的节点。
至此AQS共享锁也分析结束了。和排他锁主要区别是排他锁只有一个线程能拿到锁,共享锁可以多个线程拿到锁,并且在拿到锁的同时,也会尝试去唤醒后面的线程。