一:简介
前面我们以分析了AQS独占锁的获取与释放,本篇我们来看看共享锁,AQS对于共享锁与独占锁的实现框架还是比较类似的。
共享锁与独占锁的区别在于,独占锁是独占的,因此当独占锁已经被某个线程持有时,其他线程只能等待它被释放后,才能去争锁,并且同一时刻只有一个线程能争锁成功。
而对于共享锁而言,它可以被多个线程同时持有,因此如果一个线程成功获取了共享锁,那么其他等待在这个共享锁上的线程就也可以尝试去获取锁,而不需要等到该节点释放锁的时候。所以,在共享锁模式下,在获取锁和释放锁结束时,都会唤醒后继节点,这与独占锁有很大的区别。
二:源码分析
直接来看共享锁的获取吧:
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
对于共享锁的tryAcquireShared(int acquires)方法,它的返回值是个整型值:
- 如果该值小于0,则代表当前线程获取共享锁失败。
- 如果该值大于0,则代表当前线程获取共享锁成功,并且接下来其他线程尝试获取共享锁的行为很可能成功。
- 如果该值等于0,则代表当前线程获取共享锁成功,但是接下来其他线程尝试获取共享锁的行为会失败。
因此,只要该返回值大于等于0,就表示获取共享锁成功。
我们来看Semphore里面非公平的实现:
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
// 获取剩余的可用数
int available = getState();
// 计算剩余的与需求的差值
int remaining = available - acquires;
// 差值小于0,说明不够用,直接返回,也就是获取锁失败
// 否则CAS尝试获取
if (remaining < 0 ||
compareAndSetState(available, remaining))
// 返回剩余值,大于等于0,获取成功,小于0,获取失败
return remaining;
}
}
接下来我们看看doAcquireShared方法:
private void doAcquireShared(int arg) {
// 把线程封装成共享节点
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 拿到当前节点的前驱节点
final Node p = node.predecessor();
// 如果前驱节点是head节点,说明当前节点是队列中第一个等待锁的节点
// 那么尝试再次获取锁
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
// 获取锁成功,把当前节点设为head节点,并且唤醒后继节点
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
// 到这里要么是获取锁失败,或者队列前面有其它节点在等待锁
// 判断是否需要把当前线程挂起
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
我们看到与独占锁的获取框架大致相同,首先,获取锁失败后,把线程封装成节点,不过这里是封装成共享节点,之后如果前驱节点是head节点,说明当前节点是队列中第一个等待锁的节点,那么尝试再次获取锁,如果获取锁成功,把当前节点设为head节点,但是会唤醒后继节点,这里与独占锁不同,最后获取锁失败,或者队列前面有其它节点在等待锁,判断是否需要把当前线程挂起。
第一点不同就是独占锁的acquireQueued调用的是addWaiter(Node.EXCLUSIVE),而共享锁调用的是addWaiter(Node.SHARED),表明了该节点处于共享模式,这两种模式的定义为:
/** Marker to indicate a node is waiting in shared mode */
static final Node SHARED = new Node();
/** Marker to indicate a node is waiting in exclusive mode */
static final Node EXCLUSIVE = null;
Node被赋值给了节点的nextWaiter属性,它只起到标记作用,用作判断节点是否处于共享模式:
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
final boolean isShared() {
return nextWaiter == SHARED;
}
这里的第二点不同就在于获取锁成功后的行为,对于独占锁而言,如果获取锁成功,会把当前节点设为head节点,但是对于共享锁,还会唤醒后继节点。
我们来看setHeadAndPropagate(node, r)方法:
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
// 设置头节点
setHead(node);
// 如果还有剩余的可以获取,唤醒后继节点
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
在共享锁模式下,锁可以被多个线程所共同持有,既然当前线程已经拿到共享锁了,那么就可以直接通知后继节点来拿锁,而不必等待锁被释放的时候再通知。
共享锁的释放:
我们来看releaseShared(int arg)方法:
public final boolean releaseShared(int arg) {
// CAS释放锁的状态成功
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
如果CAS释放锁的状态成功,那么调用doReleaseShared()方法:
上面我们分析过获取共享锁成功时,也可能会调用到doReleaseShared。也就是说,获取共享锁的线程和释放共享锁的线程可能在同时执行这个doReleaseShared。
private void doReleaseShared() {
for (;;) {
// 用h来保存头节点
Node h = head;
// head != tail 说明队列中有节点在等待
if (h != null && h != tail) {
int ws = h.waitStatus;
// 后继节点需要被唤醒
if (ws == Node.SIGNAL) {
// 采用CAS操作先将Node.SIGNAL状态改为0
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
// CAS失败就重试
continue; // loop to recheck cases
// CAS成功唤醒后继节点
unparkSuccessor(h);
}
// ws为0是指当前队列的最后一个节点成为了头节点
else if (ws == 0 &&
// CAS失败,ws此时已经不为0了
// 说明有新的节点入队了,ws的值被改为了Node.SIGNAL
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
// head还是原来的那个head
if (h == head) // loop if head changed
break;
}
}
doReleaseShared使用了循环,每次循环中重新读取一次head,然后保存在h中,再配合if(h == head) break; 这样,如果检测到head没有变化时就会退出循环,不然会执行多次循环。head改变,说明锁可以被获取,那么多次循环主要是为了提升效率,快速唤醒线程。
那什么时候head会变化呢? 当有一个thread成功获取锁,然后setHead设置了新head。
当队列至少有两个node,如果状态为SIGNAL,说明h的后继是需要被通知的。通过CAS对节点的状态进行更改。只要head成功得从SIGNAL修改为0,那么head的后继的线程会被唤醒。
如果状态为0,说明h的后继所代表的线程已经被唤醒或即将被唤醒,并且这个中间状态即将消失,但是CAS设置head的status为PROPAGATE失败,要么由于acquire thread获取锁失败再次设置head为SIGNAL并再次阻塞,要么由于acquire thread获取锁成功而将自己(head后继)设置为新head并且只要head后继不是队尾,那么新head肯定为SIGNAL。所以设置这种中间状态的head的status为PROPAGATE,让其status又变成负数,这样可能被被唤醒线程检测到。
如果状态为PROPAGATE,直接判断head是否变化。
两个continue保证了进入那两个分支后,只有当CAS操作成功后,才可能去执行if(h == head) break;,才可能退出循环。
三:总结
共享锁的调用框架和独占锁很相似,它们最大的不同在于获取锁的逻辑——共享锁可以被多个线程同时持有,而独占锁同一时刻只能被一个线程持有。
由于共享锁同一时刻可以被多个线程持有,因此当头节点获取到共享锁时,可以立即唤醒后继节点来争锁,而不必等到释放锁的时候。因此,共享锁触发唤醒后继节点的行为可能有两处,一处在当前节点成功获得共享锁后,一处在当前节点释放共享锁后。