这是我参加工作之后,第一次开始写博客,我也不知道自己是怎么了,就突然想在网上沉淀点自己的东西,可能是平时在网上拿来主义太多,让我有点不太好意思吧。也可能是最近出去面试了几家公司,发现自己这两年都是在业务代码,对于底层的沉淀太少,虽然业务代码对我来说,没什么问题,但是只要涉及到底层的东西,我就会哑口无言,才让我痛下决心,一定要沉淀点属于自己的东西。不管怎么样,今天开始了这个博客,希望自己能够坚持住,保持自己的初心,持续更新技术和自己生活中的乐趣。共勉!!!
AQS就是我们在JUC包下面的AbstractQueuedSynchronizer,它的全名叫做抽象队列同步器,是用来构建锁或者其他同步器的基础组件。像我们平时使用到的ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock等,在组件的底层都是继承了同步器。
好的废话不多说,开始今天的猪脚,也就是AbstractQueuedSynchronizer源码分析:
AbstractQueuedSynchronizer实际上是由state和FIFO同步等待队列组成的,线程都会进入到这个等待队列中,等待消费消息,同时这个state是用volatile修饰的,也就是对于state的修改,都会同步到内存中,保持前后的一致性。
从上图可以看出AbstractQueuedSynchronizer继承AbstarctOwnableSynchronizer,AbstarctOwnableSynchronizer是底层的一个基础类,是一个同步器。
下面我们来看下AbstractOwnableSynchronizer源码:
//AbstractOwnableSynchronizer是抽象同步器的基础类
public abstract class AbstractOwnableSynchronizer
implements java.io.Serializable {
private static final long serialVersionUID = 3737899427754241961L;
protected AbstractOwnableSynchronizer() { }
//设置同步占用线程(transient反系列化的时候,不会出现这个对象)
private transient Thread exclusiveOwnerThread;
//设置当前拥有独占的线程,从注释中知道,当参数为null的时候,表示线程没有访问权,
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
//返回由setExclusiveOwnerThread所设置的线程,如果没有设置,则返回null。该方法不会强制执行任何同步或volatile字段访问。
protected final Thread getExclusiveOwnerThread() {
return exclusiveOwnerThread;
}
}
讲完了AbstractoOwnerSynchronizer的源码,下面让我们一起来看下AbstractQueuedSynchronizer的实现原理吧。设置给state赋值的方式有三种分别是:
setState(): 获取当前同步状态
getState(): 设置当前同步状态
compareAndSetState(): 使用CAS设置当前状态,该方法能够保证状态设置的原子性。
其中前面两个就不用解释了,我在这里着重说下第三个吧,也就是compareAndSetState(int expect, int update), 在这里的意思是:如果当前状态值等于预期值,那么自动将同步状态更新为给定的状态值。
这里先来看下compareAndSetState(int expect, int update)的源码:
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
可以看到这个方法底层是调用了unsafe类的方法这里我就不深究unsafe类,简单说下这个类的作用:
sun.misc.Unsafe()类的作用:
1. 用于扩展java语言的能力,便于在最高层(java层)实现稍底层(C语言)需要实现的东西。
2. 在这里主要为了设置CAS的头结点、CAS的尾节点、CAS的设置每一个节点的waitStatus值,CAS的设置每一个next节点的值。
好的,看到这里,前面的铺垫已经说完了,我们开始下面的分析:
AQS底层有两个同步器,分别是独占(Exclusive, 独占锁,只有一个线程能执行,如ReentrantLock)和共享(share,分享锁,可以同时运行多个线程,如Semaphore/CountDownLatch)。独占锁就是在同一时刻只能有一个线程获取到锁,而其他获取锁的线程只能处于同步队列中等待,只能获取锁的线程释放了锁,后续的线程才能够获取锁。
AQS的底层同步器主要是实现下面几个方法分别是:
//该线程是否正在独占资源,只有用到condition的时候,才会去使用它
protected boolean isHeldExclusively() {
throw new UnsupportedOperationException();
}
//独占方式,尝试去获取独占资源,成功返回true,失败返回false
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
//独占方式,尝试去释放独占资源,成功返回true,失败返回false
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
//共享方式,尝试去获取资源,负数表示失败;零表示成功,但是没有可用资源;正数表示成功,还有可用资源。
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
//共享方式,尝试去释放资源,如果释放后允许唤醒后续的节点,则返回true,否则返回false。
protected boolean tryReleaseShared(int arg) {
throw new UnsupportedOperationException();
}
同步器一般要么是独占方式,要么是共享方式,也就是tryAcquire()\tryRelease()和tryAcquireShared()\tryReleaseShared()一起使用,但是也有把独占和共享放在一起使用,如ReentantWriteAndReadLock 读写锁。
在分析的AbstractQueuedSynchronizer的时候,发现底层有一个Node类,那么这个内部类主要是做什么呢?
通过查阅资料发现,这个类主要用下面几个作用:
1. 是等待队列的Node类
2. 是线程AQS底层CAS关键内部类。
3. 关键节点waitStatus状态在Node类中实现,比如:cancelled/signal/condition/propagate。
这几个关键节点的意思如下:
"CANCELLED": 1
暗示一个线程被撤销
节点被撤销通常都是因为超时或者被中断,进入这个接口后意味着结束状态,进入该状态后节点将不会再变成。
"SIGNAL" : -1
暗示后续线程不需要被阻塞,进入等待唤醒状态的后续结点
当其前续节点调用了方法(release()或者cancel())后,应该使得其后续节点发生移动。为了避免竞争,acquire方法必须先暗示他们需要一个signal,然后重复尝试原子的acquire操作,如果失败则进入阻塞状态,再次进入等待队列的末尾,进行下一次等待。
"CONDITION" : -2
暗示一个线程处于等待中
与condition相关,该标识的结点处于等待队列中,结点的线程等待在condition上,当其他线程调用condition的signal()方法后,condition状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
"PROPAGATE": -3
暗示下一个acquireShared操作应该无条件传播
将一个释放的结点传播到其他节点,这是在doReleaseShared中设置的(仅用于头部节点),以确保传播继续,即使其他操作已经进行了干预。
0状态:值为0,代表初始化状态。
AQS在判断状态时,通过用waitStatus>0表示取消状态,而waitStatus<0表示有效状态。
4. prev链接,主要用于解决撤销操作,如果一个节点从对头删除,它的后续通常要重新连接到一个未出队的前驱节点上。
//代码中对于prev和next的解释如下:
To enqueue into a CLH lock, you atomically splice it in as new tail. To dequeue, you just set the head field.
要将其放入CLH锁中,您可以将其作为新的尾部进行拼接。对于dequeue,你只设置了head字段
+------+ prev +-----+ +-----+
head | | <---- | | <---- | | tail
+------+ +-----+ +-----+
Insertion into a CLH queue requires only a single atomic operation on "tail", so there is a simple atomic point of demarcation from unqueued to queued. Similarly, dequeing involves only updating the "head". However, it takes a bit more work for nodes to determine who their successors are, in part to deal with possible cancellation due to timeouts and interrupts.
插入到CLH队列中只需要在“tail”上执行一个原子操作,因此有一个简单的原子点,从不排队到排队。类似地,“弹出”只需要更新“头部”。然而,节点需要更多的工作来确定它们的后继者是谁,这在一定程度上是由于超时和中断可能导致的取消。
The "prev" links (not used in original CLH locks), are mainly needed to handle cancellation. If a node is cancelled, its successor is (normally) relinked to a non-cancelled predecessor.
“prev”链接(不在原始的CLH锁中使用)主要是用来处理取消的。如果一个节点被取消,它的后继(通常)会与一个未被取消的前任重新联系。
For explanation of similar mechanics in the case of spin locks, see the papers by Scott and Scherer at http://www.cs.rochester.edu/u/scott/synchronization/
We also use "next" links to implement blocking mechanics. The thread id for each node is kept in its own node, so a predecessor signals the next node to wake up by traversing next link to determine which thread it is. Determination of successor must avoid races with newly queued nodes to set the "next" fields of their predecessors. This is solved when necessary by checking backwards from the atomically updated "tail" when a node's successor appears to be null. (Or, said differently, the next-links are an optimization so that we don't usually need a backward scan.)
我们还使用“下一个”链接来实现阻塞机制。每个节点的线程id都保存在自己的节点中,因此,前一个节点将通过遍历下一个链接来确定下一个节点,以确定它是哪个线程。继任者的确定必须避免与新排队节点的竞争,以设置其前任的“下一个”字段。当一个节点的继任者看起来为null时,通过从原子更新的“尾部”向后检查,就可以解决这个问题。(或者,换个说法,下一个链接是一个优化,因此我们通常不需要向后扫描。)
5. next连接,用于阻塞机制,在每个节点都会保存自己的线程id,因此一个前驱节点标记下一个被唤醒节点是通过next连接进行遍历来决定是哪一个线程。
6. await方法:向条件队列中插入一个新节点。
7. signal方法:节点就从条件列转移到等待队列中。
弄清楚了Node类的作用,接着分析下独占模式的两个关键acquire()和release()方法:
如果获取到资源,线程直接返回,否则进入等待队列,直到获取到资源为止,且整个过程忽略中断的影响。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
流程如下:
1. 调用自定义同步器实现的tryAcquire(int arg)方法,该方法保证线程安全的获取同步状态,获取成功直接返回。
2. 如果同步状态获取失败,则构造同步节点(独占式Node.EXCLUSIVE,同一时刻只能有一个线程成功获取同步状态)并通过addWaiter(Node node)方法将该节点加入到同步队列的尾部。
3. 调用acquireQueued(Node node, int arg)方法,使得该节点以“死循环”的方式在等待队列中获取资源,一直获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
4. 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上,正好应了前面说到的acquire(int arg)方法忽略中断的影响。
在前面讲解acquire(int arg)的时候,有讲到tryAcquire(int arg)需要锁自生去实现。
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
底层只是抛出了一个异常,至于具体的实现部分,需要我们自己去实现。那么这里大概需要实现的逻辑是,尝试去获取独占资源,如果获取成功,则直接返回true;否则直接返回false。
备注:
至于这里为什么没有定义成abstract,是因为独占模式下只用实现tryAcquire-tryRelease,而共享模式下只需要实现tryAcquireShared-tryReleaseShared。如果定义成abstract的话,那么每个自定义同步器都需要实现上述的方法,这是不太需要的,是什么同步器就实现相应的方法即可,减少开发者不必要的工作,这也是源码工作者的一个比较有好的地方,赞一个。
此方法用于将获取资源的线程加入到等待队列的末尾,并返回当前线程所在的结点:
private Node addWaiter(Node mode) {
//以给定模式构造结点。mode有两种:exclusive(独占)和shared(共享)
Node node = new Node(Thread.currentThread(), mode);
// 尝试快速方式直接放到队尾
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//快速放入队列失败,则通过enq入队
enq(node);
return node;
}
同步器提供了一个基于CAS的设置尾节点的方法:compareAndSetTail(Node expect, Node update),它需要传递当前线程“认为”的尾节点和当前节点,只有设置成功后,当前节点才正式与之前的尾节点建立关联。
/**
* CAS tail field. Used only by enq.
* CAS操作失败,使用addWaiter(Node node)后面的方法
*/
private final boolean compareAndSetTail(Node expect, Node update) {
return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}
可以看到compareAndSetTail()底层调用的是unsafe类的方法。
将线程放置在等待队列的末尾
private Node enq(final Node node) {
//cas"自旋",直到成功加入队尾
for (;;) {
Node t = tail;
if (t == null) { // 队列为空,就创建一个空的标志结点作为head结点,并将tail也指向它。
if (compareAndSetHead(new Node()))
tail = head;
} else { //正常流程,放入队尾
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
这里采用“死循环”来保证节点的正确添加,在“死循环”中只有通过CAS将节点设置成为尾节点之后,当前线程才能从该方法返回,否则,当前线程不断地尝试设置。enq方法将并发添加节点的请求通过CAS变得“串行化”了。
可以看到在addWriter的时候,第一次将线程放在等待队列的末尾,如果失败,就进行enq方法,也就是再次尝试将线程放入队列的末尾务必成功。
节点进入同步队列之后,就进入了一个自旋的过程,每个节点(或者说每个线程)都在自省观察,当条件满足,获取到了同步状态,就可以从这个自旋过程中退出,否则依旧留在这个自旋过程中(并会阻塞节点的过程)。
我们再来看下acquire()方法的最后一步,线程在等待队列中获取到资源的过程:
final boolean acquireQueued(final Node node, int arg) {
//标记是否成功拿到资源
boolean failed = true;
try {
//标记等待过程中是否被中断过
boolean interrupted = false;
//自旋
for (;;) {
//拿到前驱
final Node p = node.predecessor();
//如果前驱是head,即该结点已成为signal,那么便有资格去尝试获取资源(可能是老大释放完资源唤醒自己的,当然也可能是被interrupt。)
if (p == head && tryAcquire(arg)) {
//拿到资源后,将head指向该结点。所以head所指的标杆结点,就是当前获取到资源的那个结点或null。
setHead(node);
//setHead中node.prev已置为null,此处再将head.next置为null,就是为了方便GC回收以前的head结点,也就意味着之前拿完资源的结点出队。
p.next = null; // help GC
failed = false;
//等待过程中被中断返回false
return interrupted;
}
//如果自己可以休息了,就进入waiting状态,直到被unpark()
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
//如果等待过程中被中断过,哪怕只有那么一次,就将interrupted标记为true。
interrupted = true;
}
} finally {
if (failed)
//通过分析,这个方法一般情况下是不会执行,只要是上次的死循环正常进行,failed的boolean值就是fasle,不会进行到这个方法里面,而为什么这里在finally里面写上这个方法呢,主要是为了在上面死循环的过程中,出现意外情况下,保证原子的一致性,用于取消线程正在进行的尝试获取资源。
cancelAcquire(node);
}
}
上面源码看的差不多,先不着急总结acquireQueued(Node node, int arg)方法,先来看下这个方法里面调用的shouldParkAfterFailedAcquire(Node pred, Node node)方法和parkAndCheckInterrupt()方法。
备注:
这里我想补充下,在检查线程是否中断和是否正确让节点进入waitStatus状态中,有一个设计比较有意思的地方就是,它将是否中断改成了false,但是它又没有立刻将程序中断,而是等待线程获取到资源后,才去检查线程是否中断。非常有意思的一个设计,正好和我前面将acquire()方法的时候说到,线程在获取资源的过程中忽略线程被中断的影响,继续执行,只在获取到资源后,才去检查线程是否中断过。
我们看下方法名,就大概能猜到这个方法的意义,“尝试获取资源失败后,等待节点被唤醒”。哈哈,是不是很有意思的翻译,原谅我蹩脚的英语,大概就是这么个意思吧。话不多,我们来看下源码的含义:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//拿到前驱的状态
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* 如果已经告诉前驱拿完号后通知自己一下,那就可以安心休息了。
*
*/
return true;
if (ws > 0) {
/*
* 如果前驱放弃了,那就一直往前找,直到找到最近一个正常等待的状态,并排在它后边。
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* 如果前驱正常,那就把前驱的状态设置为signal,告诉它拿完号后通知自己一下。有可能失败,前线程刚释放完,后再度获取资源。(独占的精髓,饥饿竞争,哈哈,这是我自己的一点小理解。)
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
整个流程中,如果前驱结点的状态不是signal,那么自己就不能安心的去休息,需要去找一个安心休息点,同时可以再尝试下看有没有机会轮到自己拿号。
好的,这里我再尝试翻译下,“等待被唤醒和检查中断”,哈哈哈,忍住我要憋笑,大概就是这么一个意思吧。看下源码:
private final boolean parkAndCheckInterrupt() {
//调用park()使线程进入waiting状态
LockSupport.park(this);
//如果被唤醒,查看自己是不是被中断
return Thread.interrupted();
}
park()会让当前线程进入waiting状态。在此状态下,有两种途径可以唤醒该线程:
1. 被unpark()
2. 被interrupt()
看完shouldParkAfterFailedAcquire()和parkAndCheckInterrupt()方法后,总结下acquireQueued()方法的具体流程:
1. 结点进入队尾后,检查状态,找到安全休息点。
2. 调用park()进入waiting状态,等待unpark()或者interrupt()唤醒自己。
3. 被唤醒后,看自己是不是有资格能拿到号。如果拿到,head指向当前结点,并返回从入队到拿到号的整个过程中是否被中断过;入股没拿到,继续流程1,直到自己拿到号。
到这里,acquire(int arg)方法里面的源码也分析的差不多,来大概的总结一下:
1. 调用自定义同步器的tryQcquire()尝试直接去获取资源,如果成功则直接返回。
2. 没成功,则addWaiter()将该线程加入等待队列的尾部,并标记为独占模式。
3. acquireQueued()使线程在等待队列中休息,有机会时(轮到自己,会被unpark())会去尝试获取资源。获取到资源后才会返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
4. 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。
在这里我借用下,别的博客大神的图,文未我附上摘自哪位大神的博客:
上面的acquire()方法告一段落,再来分析下独占模式的release()方法吧。
独占式的释放同步状态,该方法会在释放同步状态之后,将同步队列中第一个节点包含的线程唤醒。
public final boolean release(int arg) {
if (tryRelease(arg)) {
//找到头结点
Node h = head;
if (h != null && h.waitStatus != 0)
//唤醒等待队列里的下一个线程
unparkSuccessor(h);
return true;
}
return false;
}
其实这个方法比acquire()方法简单很多,release顾名思义就是为了释放资源,有一点需要注意,它是根据release()返回值来判断线程是否已经完成释放掉资源,so在设置自定义同步器的时候,需要注意tryRelease一定要明确的指出这一点。
接下来看下tryRelease()源码,其实和tryAcquire()是一样的,需要同步器自己去实现里面的原理。
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
一般情况下,tryRelease都会返回成功,独占模式下,线程前来释放资源,一般都是已经拿到独占资源,直接减掉相应量的资源即可(state=arg,state=0),也不需要考虑线程安全的问题,但是需要注意的一点就是,在实现自定义同步器的时候,一定要彻底释放资源(state=0),才能返回true,其他情况一定是返回false。
来了老弟,说不多话,我来小译下“唤醒继承人”,prefer完美,对了,就是这么个意思。在前面说到,等待线程中,有一个机制,前驱节点释放资源后,会去唤醒下个结点进入waitStatus状态,上源码:
private void unparkSuccessor(Node node) {
/*
* node为当前线程所在的结点
*/
int ws = node.waitStatus;
if (ws < 0)
//置零当前线程所在的结点状态,允许失败
compareAndSetWaitStatus(node, ws, 0);
/*
* 找到下一个需要唤醒的结点
*/
Node s = node.next;
//如果为空或已取消
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
//这里可以看出,<=0结点,都是有效的结点
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
//唤醒操作
LockSupport.unpark(s.thread);
}
这里用unparkSuccessor(Node node)唤醒等待队列中最前边的那个未放弃线程。
release()是独占模式下线程释放共享资源的顶层入口,它会释放指定量的资源,如果彻底释放了(state=0),它会唤醒等待队列里的其他线程来获取资源。
好,分析完独占模式,接下来再来分析下共享模式,了解两个模式的底层运行原理,有助于我们更加有利的知道各种锁的实现机制。通过方法名caquireShared“共享获取资源”,它会获取指定量的资源,获取成功则直接返回,获取失败则进入等待队列,直到获取到资源为止,整个过程忽略中断的影响,和独占模式下acquire是一样的道理。
public final void acquireShared(int arg) {
//自定义同步器,获取资源同享的资源
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
可以看出,tryAcquireShared(int arg)同样需要同步器自定义去实现相应的逻辑,但是这里要一个主要注意的地方,负值代表获取失败;0代表获取成功,但没有剩余资源;正数表示获取成功,还有剩余资源,其他线程还可以去获取。
so这里acquireShared()流程为:
1. tryAcquireShared()尝试获取资源,成功则直接返回
2. 失败则通过doAcquireShared()进入等待队列,直到获取到资源为止才返回。
此方法用于将当前线程加入等待队列尾部休息,直到其他线程释放资源唤醒自己,自己成功拿到相应的资源后才返回。
private void doAcquireShared(int arg) {
//加入队列尾部
final Node node = addWaiter(Node.SHARED);
//是否成功标志
boolean failed = true;
try {
//等待过程中是否被中断过的标志
boolean interrupted = false;
//运用CAS死循环,等待想要的结果
for (;;) {
//前驱
final Node p = node.predecessor();
//如果head的下一个,因为head是拿到资源的线程,此时node被唤醒,很可以是head用完资源来唤醒自己的
if (p == head) {
//尝试获取资源
int r = tryAcquireShared(arg);
//成功获取资源
if (r >= 0) {
//将head指向自己,还有剩余资源可以再唤醒之后的线程
setHeadAndPropagate(node, r);
//将前置节点置为null,释放完资源等待GC的回收。
p.next = null;
//这里是一个关键的地方,等到线程获取到资源之后,
//才会去检查该线程有没有中断过,中断就直接返回false
if (interrupted)
//线程被中断,释放资源
selfInterrupt();
failed = false;
return;
}
}
//判断状态,寻找安全点,进入waitStatus状态,等着呗unpark()或者interrupt()
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
总体来说和独占模式下的acquireQueued()是非常的相似,就不做过多的解释,详情看源码上的注释。
设置头结点,并唤醒其他线程。
//如果后续节点在共享模式下处于等待状态,则设置队列投节点且进行检查
//如果参数propagate>1或者propagate状态值被设定,则进行传播
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // 记录原头节点以用于下面的检查操作
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();
}
}
这个方法在setHead()的基础上多了一步,就是自己苏醒的同时,如果条件符合(比如还有剩余资源),还会去唤醒后续结点。
acquireShared()方法步骤:
1. tryAcquireShared()尝试获取资源,成功则直接返回
2. 失败则通过doAcquireShared()进入等待队列park(),直到被unpark()/interupt()并成功获取到资源才返回。整个等待过程也是忽略中断。
其实跟acquire()流程大同小异,只不过多了个自己拿到资源后,还会去唤醒后续结点的操作,这是共享的精髓所在。
分析完acquireShared()之后,再来看下与之对应的releaseShared()释放资源方法。
public final boolean releaseShared(int arg) {
//尝试释放资源
if (tryReleaseShared(arg)) {
//唤醒后续结点
doReleaseShared();
return true;
}
return false;
}
此方法的意义在于,它会释放指定量的资源,如果成功释放且允许唤醒等待线程,它也会唤醒等待队列里的其他线程来获取资源。同样tryReleaseShared(int arg)跟其他的一样,需要同步器自定义去实现这个共享释放方法。
注意:
releaseShared和release有一点不太一样的地方,独占模式下的tryRelease()在完全释放掉资源(State=0)后,才会返回ture去唤醒其他线程,这主要是基于独占下可重入的考量;而共享模式下的releaseShared()则没有这种要求,共享模式实现就是控制一定量的线程并发执行,那么拥有资源到线程在释放部分资源时就可以唤醒后续等待结点。例如,资源总量是13,A(5)和B(7)分别获取到资源并发运行,C(4)来时只剩1个资源就需要等待。A在运行过程中释放掉2个资源量,然后tryReleaseShared(2)返回true唤醒C,C一看只有3个仍不够继续等待;随后B又释放2个,tryReleaseShared(2)返回true唤醒C,C一看有5个够自己用了,然后C就可以跟A和B一起运行。而ReentrantReadWriteLock读锁的tryReleaseShared()只有在完全释放掉资源(state=0)才返回true,所以自定义同步器可以根据需要决定tryReleaseShared()的返回值。
主要用于唤醒后续的结点。
private void doReleaseShared() {
for (;;) {
Node h = head;
//如果头结点不为null且队列中节点个数>1
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
}
//如果头结点发生更改,则继续循环
if (h == head)
break;
}
}
在这里已经分析完了AbstractQueuedSynchronizer内部的两种模式:独占(acquire和release)和共享(acquireShared和releaseShared),基本上了解了AQS同步框架的底册实现机制。
后面我会附上使用的方式,由于这里篇幅太长,就不写在这里。
最后谢谢大家的耐心观看!!!
文章中结构和部分内容摘自如下博客,敬请原谅:
https://www.cnblogs.com/waterystone/p/4920797.html
https://blog.csdn.net/caoxiaohong1005/article/details/80173470
https://blog.csdn.net/blogs_broadcast/article/details/80723021