(一)、概述
这篇文章将会分析AQS源码之共享锁,我在上一篇文章中已经分析过AQS的独占锁以及AQS的底层实现原理,如果还没有看过的朋友,建议先看一下,因为这样理解共享锁会更加容易。
共享锁:允许在同一时刻多个线程持有锁。
共享锁的特点: 当一个共享锁的线程被唤醒,那么它同时也会唤醒在同步队列中的下一个线程,如果这个线程也是等待共享锁的话。但是如果下一个线程在等待的是独占锁的话,那是无法获取的。
(二)、共享锁的获取
public final void acquireShared(int arg) {
//尝试获取锁
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
首先来看这个方法,首先调用了tryAcquireShared方法,这同样是一个protected方法,只是简单地抛出一个异常,需要子类进行具体的实现。tryAcquireShared()方法的作用是尝试获得共享锁。
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
接着调用了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();
//如果当前节点是头节点
if (p == head) {
//尝试获得共享锁
int r = tryAcquireShared(arg);
//如果大于0,则需要唤醒后继节点
if (r >= 0) {
//让之后等待同步锁的线程进行唤醒
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);
}
}
上面这个方法中特别需要注意的是tryAcquireShared(),这个方法会返回一个数字。当返回值小于0的时候,表示获取锁失败。当返回值等于0的时候,表示获取锁成功,但不唤醒后面的线程。当返回值大于0的时候,需要尝试唤醒后面等待共享锁的线程。
下面来看一下setHeadAndPropagate()如何唤醒后面排队的线程:
private void setHeadAndPropagate(Node node, int propagate) {
//获得头节点
Node h = head; // Record old head for check below
//将node节点设置为头节点
setHead(node);
//这里有两种情况需要被唤醒
//1、propagate > 0,表示需要唤醒后继节点
//h.waitStatus < 0,头节点的后继节点需要被唤醒
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
//判断是否存在后继节点,以及后继节点是否是在等待共享锁,而不是独占锁的
if (s == null || s.isShared())
//唤醒,这个后面会详细解释
doReleaseShared();
}
}
上面的方法中通过判断下一个节点是否为null以及是否是在等待共享锁,进而调用doReleaseShared()方法,这个方法主要是用来唤醒其他线程的。
private void doReleaseShared() {
//死循环,自旋操作
for (;;) {
//获得头节点
Node h = head;
//判断头节点是否为null,以及是否只有一个结点
if (h != null && h != tail) {
//如果不是,获取头节点的状态
int ws = h.waitStatus;
//如果状态是SIGNAL,那么就是会唤醒下一个线程
if (ws == Node.SIGNAL) {
//CAS操作将状态置为0
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
//执行唤醒操作
unparkSuccessor(h);
}
//如果后继节点不需要唤醒,则把当前节点设置为PROPAGATE确保状态可以传递下去
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
//如果头节点没有发生变化,表示设置完成,退出循环
//如果头节点发生变化,比如说其他线程获取到锁,为了使唤醒动作可以传递,需要进行重试。
if (h == head) // loop if head changed
break;
}
}
上面执行唤醒操作调用了unparkSuccessor执行唤醒操作:
private void unparkSuccessor(Node node) {
//获取节点的状态
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
//将s赋值为当前节点的下一个节点
Node s = node.next;
//如果节点s不需要被唤醒
if (s == null || s.waitStatus > 0) {
s = null;
//从后往前遍历,离头节点最近的需要被唤醒的节点
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
//调用unpark()唤醒节点。
if (s != null)
LockSupport.unpark(s.thread);
}
(三)、共享锁的释放
public final boolean releaseShared(int arg) {
//释放锁
if (tryReleaseShared(arg)) {
//唤醒后面的线程
doReleaseShared();
return true;
}
return false;
}
从上面的方法中可以看到,先尝试释放锁,如果释放成功,就尝试唤醒下一个在等待共享锁的线程。
(四)、总结
其实共享锁和独占锁的实现原理都是差不多。共享锁的主要特征在于当一个在等待队列中的共享节点成功获取到锁以后(它获取到的是共享锁),既然是共享,那它必须要依次唤醒后面所有可以跟它一起共享当前锁资源的节点,毫无疑问,这些节点必须也是在等待共享锁(这是大前提,如果等待的是独占锁,那前面已经有一个共享节点获取锁了,它肯定是获取不到的)。
参考资料:AQS之共享锁