AQS是什么?
全称AbstractQueuedSynchronizer 抽象队列同步器,是java.util.concurrent.locks包下的基础组件,
是用于构建锁和同步器的框架,通俗的理解就是多线程环境下,AQS封装了一揽子对临界资源线程安全的操作,以ReentrantLock来看:
获取锁?成功则锁住,否则放入FIFO队列等待获取锁(线程挂起或者说阻塞),锁的释放等这些底层操作都由AQS封装;
许多同步器都可以通过AQS很容易并且高效的构造出来
AQS在Java并发编程中扮演什么角色?
java.util.concurrent包下许多可阻塞类的底层实现,如ReentrantLock, CountDownLatch, Semaphore,
ReentrantReadWriteLock, SynchronousQueue和FutureTask都是基于AQS构建,
可见搞懂AQS在阅读java并发包下的其他组件时是多么必要!!!
AQS核心是什么?
包含三部分核心内容,状态、队列、以及由具体工具类去实现的获取/释放等方法
/**
* The synchronization state.
*/
private volatile int state;
-
state状态:如上定义, 它的含义也会由具体的实现类不同而具有不同的含义,来看看几个常用的同步器中的state含义:
- ReentrantLock:state状态用于表示重入次数,重入1次state加1,释放一次state减1,state为零表示没有任何线程占用锁
- Semaphore同步器:state状态用于保存当前可用许可的数量,当尝试拿一个许可并成功拿到时,相应的许可数量减去;
如果尝试释放并成功释放许可时,许可数量将会增加 - CountDownLatch同步器:和Semaphore和相似,state状态保存的是当前的计数值,countDown方法调用release,从而导致计数值递减,
并且当计数值为零时,解除所有线程的阻塞;await调用acquire,当计数器为零时,acquire将立即返回,否则将阻塞 - FutureTask:state状态保存任务的状态,如正在运行、已完成或已取消
- ReentrantReadWriteLock:此实现有两把锁分别是读锁和写锁,state状态使用16位的状态来表示写入锁的计数,使用另外16位的状态来表示读取锁的计数
-
FIFO队列 先进先出队列
- 主要的作用是用来存储等待的线程。比如很多线程都想要去抢夺这把锁,但是大部分都是抢不到的,该如何处理这些线程呢?
那就将它们放进队列里面,然后进行处理 - 当多个线程去竞争同一把锁的时候,就需要排队机制把那些没能拿到锁的线程串联起来,当前面的线程释放锁之后,这个管理器就会挑选一个合适的线程来尝试抢刚才被释放的那把锁
所以AQS就一直维护这个队列,并把等待的线程都放进队列里面 - 队列内部是双向链表,结构看起来简单,但是要线程安全的维护这个队列,也着实不容易…
- 主要的作用是用来存储等待的线程。比如很多线程都想要去抢夺这把锁,但是大部分都是抢不到的,该如何处理这些线程呢?
-
获取/释放方法
- AQS使用模版方法模式,将tryAcquire,tryRelease等具体获取和释放资源的方法提供给子类实现,这里的资源主要针对的是state值的处理,来看一个例子就明白了
/**
* <p>Here is a non-reentrant mutual exclusion lock class that uses
* the value zero to represent the unlocked state, and one to
* represent the locked state. While a non-reentrant lock
* does not strictly require recording of the current owner
* thread, this class does so anyway to make usage easier to monitor.
* It also supports conditions and exposes
*/
class Mutex implements Lock, java.io.Serializable {
// Our internal helper class
// 这里去继承AQS并实现相关方法
private static class Sync extends AbstractQueuedSynchronizer {
// Reports whether in locked state
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
// 使用者实现
// Acquires the lock if state is zero
@Override
public boolean tryAcquire(int acquires) {
assert acquires == 1; // Otherwise unused
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
// 使用者实现
// Releases the lock by setting state to zero
@Override
protected boolean tryRelease(int releases) {
assert releases == 1; // Otherwise unused
if (getState() == 0) {
throw new IllegalMonitorStateException();
}
setExclusiveOwnerThread(null);
setState(0);
return true;
}
// Provides a Condition
Condition newCondition() { return new ConditionObject(); }
// Deserializes properly
private void readObject(ObjectInputStream s)
throws IOException, ClassNotFoundException {
s.defaultReadObject();
setState(0); // reset to unlocked state
}
}
// The sync object does all the hard work. We just forward to it.
private final Sync sync = new Sync();
@Override
public void lock() { sync.acquire(1); }
@Override
public boolean tryLock() { return sync.tryAcquire(1); }
@Override
public void unlock() { sync.release(1); }
@Override
public Condition newCondition() { return sync.newCondition(); }
public boolean isLocked() { return sync.isHeldExclusively(); }
public boolean hasQueuedThreads() { return sync.hasQueuedThreads(); }
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
}
//
上面通过继承AbstractQueuedSynchronizer实现的非重入独占锁,state=0表示可以获取锁,state=1表示次锁已经被持有了,
可以看见通过重写tryAcquire,tryRelease方法对具体资源的处理
AQS核心原理
-
state:状态量 也就是资源,实际获取、释放都是通过围绕state状态量判断
private volatile int state;
-
等待队列:实际是通过双向链表实现的队列,分别有队头引用head, 队尾引用tail
- head: 特别需要注意的是, head对应的线程已经是获得了资源,因此我们常说的唤醒是针对的除head节点之外的其他节点
- 等待队列:强调上一条,真正等待唤醒的节点是除head之外的其他节点
- 等待队列中被唤醒的节点只有获取资源成功了才能设置成head
- 同时需要注意的是,只有前驱节点是head的节点才有机会尝试获取资源
final Node p = node.predecessor();
if (p == head && tryAcquire(arg))
-
AQS大致的思路:定义state资源,作为能否获取成功的依据,如果获取失败(那也得想办法处理不是?),就放进AQS维护的一个等待队列,
等待资源释放了,就尝试唤醒后继节点(一般是这样,但如果是cancel的需要排除队列),被唤醒的节点尝试去获取资源,这个时候也可能会失败的哈,
比如ReentrantLock的非公平锁实现中,每次tryAcquire都要尝试去获取资源(这里就和等待队列里被唤醒的节点是竞争关系),通过CAS+自旋的方式保证线程安全性 -
Node节点定义:
static final class Node {
// 表示当前节点在共享模式上等待
static final Node SHARED = new Node();
// 表示当前节点在独占模式上等待
static final Node EXCLUSIVE = null;
// waitStatus有5个值,初始值=0
// CANCELLED = 1
// SIGNAL = -1
// CONDITION = -2
// PROPAGATE = -3
volatile int waitStatus;
volatile Node prev; // 前驱节点
volatile Node next; // 后继节点
volatile Thread thread; // 此节点对应的线程
Node nextWaiter;
....
- SIGNAL = -1,表明后继节点需要被当前节点唤醒
- CANCELLED = 1,由于超时或中断,该节点被取消,节点永远不会离开此状态。特别的是,具有取消节点的线程永远不会再次阻塞
- CONDITION = -2,表示结点线程等待在condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁
- PROPAGATE = -3,共享模式下,前驱节点不仅会唤醒其后继节点,同时也可能唤醒后继节点的后继节点
- head,tail节点:分别是队头,队尾节点
/**
* Head of the wait queue, lazily initialized. Except for
* initialization, it is modified only via method setHead. Note:
* If head exists, its waitStatus is guaranteed not to be
* CANCELLED.
*/
// 等待队列头节点,只能通过方法setHead设置head,如果head节点存在,
// 它的waitStatus状态一定不会是CANCELLED
private transient volatile Node head;
/**
* Tail of the wait queue, lazily initialized. Modified only via
* method enq to add new wait node.
*/
// 等待队列尾节点,只会在enq方法中添加到队列
private transient volatile Node tail;
源码分析
独占模式
1、acquire方法
忽略中断的独占模式的入口方法
- 所谓忽略中断,就是当线程发生中断的情况仅仅只是记录中断状态,而不会类似于抛出中断异常来响应,具体响应中断可由实现类处理
// arg参数表示的需要获取的资源量
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
主要操作:
- 首先通过tryAcquire尝试获取资源,如果获取成功这里就结束了;tryAcquire方式使用的模版方法模式来设计,由实现类去实现具体如何获取资源这个操作
- 如果获取失败了,通过addWaiter方法把当前线程封装成一个Node节点放进等待队列队尾
- acquireQueued核心方法,通常等待队列里的节点在尝试获取锁失败之后,都会阻塞在此放方法中(park操作,也就是线程挂起),
当节点被唤醒时,又从被挂起的地方开始尝试获取资源;因为在被阻塞的过程中,此线程可能被中断了,所以需要记录中断标志(不立即响应),交给上层处理,acquireQueued返回值就代表释放已经中断 - selfInterrupt就是检测到中断后,设置中断标志
// 此操作会清除中断标志
Thread.interrupted()
// 所以这里需要重新设置中断标志
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
1.1 addWaiter
简单来说就是将当前线程封装成Node节点,添加至队列尾部
/**
* Creates and enqueues node for current thread and given mode.
*
* @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
* @return the new node
*/
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
// 如果tail为空说明队列还没有初始化,需要通过enq进行处理
if (pred != null) {
node.prev = pred;
// 如果tail节点设置成功了就直接返回,否则通过enq处理(毕竟多线程环境CAS操作也有可能失败嘛)
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
1.1.1 enq方法
自循环的方式将node加入队列尾部,直到成功
// 返回当前节点node的前驱节点
private Node enq(final Node node) {
for (;;) {
Node t = tail;
// 如果还没有tail节点,说明队列还没有初始化
if (t == null) { // Must initialize
// 注意这里先加入的是一个空Node节点,同时也没有直接退出enq方法
// 也就是上面说的,除head之外 等待队列里的其他节点才是真正在阻塞状态的节点
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
// CAS设置成了就直接返回
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
这一通过自旋的方式保证能够将node节点添加至等待队列尾部
1.2、acquireQueued
独占不响应中断模式尝试获取资源,如果此节点node的前驱节点是head,那它才有资格去尝试获取资源,获取失败之后挂起线程,进入阻塞状态
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
// 记录中断状态,用做方法返回
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor(); // 前驱节点
// 如果是前驱是head节点才去尝试获取资源
if (p == head && tryAcquire(arg)) {
// 只有获取资源成功之后才重新设置head为当前节点node
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 判断获取资源失败之后应该挂起线程
if (shouldParkAfterFailedAcquire(p, node) &&
// 挂起线程,并在被唤醒之后检查线程中断状态
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
// 如果此节点node在尝试获取资源的过程中出现了异常,那就直接从等待队列里面取消此节点
if (failed)
cancelAcquire(node);
}
}
//
主要操作:
- 如果当前node节点的前驱是head节点,那么才有资格去尝试获取资源
- 如果获取资源成功之后,才会设置head为当前节点node
- 在获取资源失败之后一般就会考虑是否需要挂起线程
- 挂起的线程在被唤醒之后就会继续从挂起前的地方开始操作,这个时候需要检查一下中断标志,如果中断了需要记录一下,方便返回
1.2.1、shouldParkAfterFailedAcquire
判断是否应该挂起当获取资源失败之后
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
// 这里是拿到的是前驱节点的状态,当前节点node需要确保前驱节点是SIGNAL状态,因为只有这样
// 前驱节点在释放资源之后才会唤醒当前节点node,也就是当前节点才能放心的去挂起
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) { // 如果前驱节点被取消了,那不断尝试往前找未取消的前驱节点
// 这是一定能找到的,因为至少head节点是不可能被取消的
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
// 这里并不会直接挂起当前节点,而是设置前驱节点的waitStatus为SIGNAL,表明在它释放资源之后需要唤醒当前节点
// 为啥不直接挂起当前节点node呢?采用的做法是在线程挂起之前再尝试一次获取资源,如果成功就不需要挂起了
// 考虑这样一个场景:队列里有一个head和一个刚加入队列的节点a,节点a尝试过一次获取资源,但是失败了,因为head节点还没有释放资源
// 在节点a判断是否需要挂起之前,这个时候正好head节点释放资源,这个时候节点a只要再尝试一次获取资源也就可以成功了
// 毕竟挂起和唤醒线程的消耗是高于自旋等待的消耗
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
//
主要操作
- 当前节点node需要确保前驱节点是SIGNAL状态后 它才放心把自己挂起(因为会有人唤醒自己嘛)
- 需要注意的是:waitStatus大于0表示节点是取消状态,只有小于等于0才是有效状态
- 当前驱节点的ws大于0了说明前驱节点被取消了,需要为当前节点找到一个未被取消的前驱节点,这个前驱节点一定是能找到的,至少head节点是不可能被取消的
- 否则的话需要将前驱节点的状态设置为SIGNAL,表明前驱节点释放资源后需要通知到自己(也就是当前node节点);当前节点node再经过一轮尝试获取资源失败后就可以直接挂起了
1.2.2、parkAndCheckInterrupt
挂起当前线程,并检查中断标志
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
借助LockSupport#park将当前线程挂起,同时通过Thread.interrupted()检查当前线程的中断标志,需要注意的是,
此方法会清除中断标志,因此如果需要保存中断状态的话,在上层需要再重新设置
1.2.3、cancelAcquire
取消需要获取资源的node
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null)
return;
node.thread = null;
// Skip cancelled predecessors
Node pred = node.prev;
// 找到未被取消的前驱节点
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// predNext is the apparent node to unsplice. CASes below will
// fail if not, in which case, we lost race vs another cancel
// or signal, so no further action is necessary.
Node predNext = pred.next;
// Can use unconditional write instead of CAS here.
// After this atomic step, other Nodes can skip past us.
// Before, we are free of interference from other threads.
node.waitStatus = Node.CANCELLED;
// If we are the tail, remove ourselves.
// 如果当前节点node为tail,那就重新将pred设置为tail
if (node == tail && compareAndSetTail(node, pred)) {
// 设置成功之后,因为pred为tail节点,肯定是没有后继节点了
compareAndSetNext(pred, predNext, null);
} else {
// If successor needs signal, try to set pred's next-link
// so it will get one. Otherwise wake it up to propagate.
int ws;
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
Node next = node.next;
if (next != null && next.waitStatus <= 0)
// 相当于将node节点从队列里面排除了
compareAndSetNext(pred, predNext, next);
} else {
// 试想当前node节点是等待队列的第一个节点(head除外),如果它被取消了,是不是得担负起唤醒后继节点的责任?
// 假如节点node是head节点释放资源后唤醒的一个节点,而节点node在获取资源的时候发生了异常,node节点需要被取消
// 但是传播唤醒这个责任是不是就落到了node节点的头上? (毕竟是head唤醒了你,而你又要跑路,你是不是得先把唤醒责任处理好了再跑呢?)
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
//
主要操作:
- 什么时候会被取消?从源码注释来看,当超时或者响应中断的时候会进行取消
- 一般情况下将取消的节点从链表中移除即可
- 如果当前node节点的前驱节点是head,自己(node)本来是被唤醒去获取资源的,现在被取消要删除了,后面的 “兄弟"谁去唤醒呢?
因此这种情况下,我(node)需要唤醒一个后面的"兄弟”,才能安静的离去…
2、release
独占模式下的资源释放
public final boolean release(int arg) {
// 通过tryRelease去尝试释放资源
if (tryRelease(arg)) {
Node h = head;
// head不为空说明等待队列里面有待唤醒的节点,waitStatus=0表示后继节点不需要唤醒,
// 因此需要满足h.waitStatus!=0才能去唤醒后继节点
if (h != null && h.waitStatus != 0)
// 尝试唤醒后继节点
unparkSuccessor(h);
return true;
}
return false;
}
主要操作:
- 通过tryRelease尝试去释放资源,这是利用模版方式模式,具体由子类去实现
- 满足待唤醒条件后,通过unparkSuccessor去唤醒
2.1 unparkSuccessor
唤醒后继节点
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
// 如果ws < 0表明是正常状态,置为0表示不需要唤醒后继节点这类操作
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
// ws大于0表示已经取消了,那就找到第一个未取消的节点来唤醒,不过这里是通过从后往前扫的方式处理的
// 通常情况下是唤醒node.next节点,但是考虑到节点被取消或者是null的情况,需要从后面往前扫
Node s = node.next;
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;
}
// 唤醒线程
if (s != null)
LockSupport.unpark(s.thread);
}
共享模式
此模式下需要搞清楚tryAcquireShared方法返回的三类数值
- 如果返回小于0的值:表示此次获取资源失败
- 如果返回等于0的值:表示此次获取资源成功,但是资源已经用尽了,不能向后传播唤醒操作了(因为没资源可用的,只能等到释放之后)
- 如果返回大于0的值:表示此次获取资源成功,同时下一次也是有可能成功获取资源,唤醒操作向后传播(毕竟有资源可用嘛)
需要注意的是:获取资源成功之后会进行的setHead操作,也就是head会被改变,但并不是说之前head对应的节点已经释放资源结束了,换句话说新head节点获取资源运行的同时,旧head节点仍然可能在运作中,其携带的资源并未释放
举一个实际例子来看看:
- 假设资源总量为10,现在有4个线程a,b,c,d想要获取资源运行,需要的资源分别是5,3,3,4
- 这个时候假设a,b已经抢占资源并运行了,占用资源数为8,剩余资源2;需要注意的是线程a,b可不是等待队列的节点哈
- 此时线程c来了,想要获取资源,但是 3 > 2,获取失败,进入等到队列
- 线程d也来了,同样4 > 2获取失败,进入等待队列
- 此时等待队列的状态:两个节点等待唤醒,head节点中的Node是通过new Node()方式的"假节点",也就是没有实际运行的线程
- 这个时候线程a运行结束,释放资源数量为5,此时资源剩余总量为7,尝试唤醒线程c(优先队列,先唤醒最开始的嘛),
线程c唤醒后成功获取资源,并设置head为线程c所在节点,此时资源剩余量为4;需要注意的是这里head已经变了(这里是关键),
说明前一个被唤醒的节点已经成功获取了资源,那么很有可能还有可用资源,OK,那根据共享模式的传播特性,再去唤醒下一个共享模式的节点,
因此线程d对应的节点也被唤醒了,并成功获取资源;这个时候在运行中的线程有b,c,d
从这个例子可以看出,它并不是盲目唤醒后继节点,而是根据head变化来判断上一个唤醒的节点是否已经成功获取了资源,如果是则说明还有资源可用
否则的话,认为没有资源可用,不在唤醒后继节点了(这里还有一个问题,如果可用资源是4,等待队列中的节点a,b所需资源分别是5,4,但由于顺序关系
节点a被唤醒,尝试获取资源失败,此时尽管b节点所需资源满足要求,但是不会唤醒b节点,传播终止…)
1、acquireShared
共享模式下获取资源
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
- 通过tryAcquireShared方法尝试获取资源,也是模版方法,由子类具体实现
- 如果获取失败,就尝试放进等待队列
1.1 doAcquireShared
获取资源
/**
* Acquires in shared uninterruptible mode.
* @param arg the acquire argument
*/
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为当前节点,同时可能的话尝试唤醒后继共享模式的节点
// 这里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);
}
}
看起来和独占模式的获取资源很像吧,有一点区别在于setHeadAndPropagate
- 独占模式下,获取资源成功之后通过setHead改变head就可以了
- 在共享模式下,通过setHeadAndPropagate方法更改head, 同时 如果资源剩余量r>0说明有资源可用,尝试唤醒后继共享模式下的节点去获取资源
相比独占模式多了一个唤醒传播机制
1.1.1 setHeadAndPropagate
设置head并尝试唤醒后继节点
// 这里参数propagate就是tryAcquireShared方法的当前资源剩余量
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
// propagate > 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();
}
}
1.1.2 doReleaseShared
共享模式下,唤醒后继节点以确保传播性;而独占模式的仅在资源释放时,才会去获取head节点的后继节点
private void doReleaseShared() {
// 这里使用自循环是保证在CAS失败之后也要进行重试
for (;;) {
Node h = head;
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);
}
// 后继节点不需要唤醒的话,将h的ws设置为PROPAGATE,以确保release之后的传播性
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
// 这里很关键,如果head改变了就意味着上一个被唤醒的节点已经成功获得了资源
// 也就是也许还有资源可以获取,因此尝试唤醒后继节点,以此类推...
if (h == head) // loop if head changed
break;
}
}
//
需要关注两点:
- 使用自循环是保证在CAS失败之后也要进行重试
- 通过head来判断被唤醒的节点是否已经获取资源(毕竟获取资源成功之后就会改变head的值);
如果head改变了就意味着上一个被唤醒的节点已经成功获得了资源, 也就是也许还有资源可以获取,因此尝试唤醒后继节点,以此类推…
2、releaseShared 释放资源
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
// 这里也是通过doReleaseShared来释放资源
doReleaseShared();
return true;
}
return false;
}
小结
从源码层面来看分析了独占模式和共享模式的获取、释放资源相关源码,其中还有其配套的响应中断的独占模式 和 响应中断的共享模式,
结构上基本一致,只不过在对遇到中断的情况下做了中断响应,这里不在赘述,有兴趣可以看看源码。
整个AQS的结构都是围绕着三个核心点展开
- state状态: 不同的实现类有不同的含义,通俗的理解就是资源
- FIFO队列: 当尝试获取资源失败之后,得想一个办法把失败的线程给存起来,这里也就是给放进队列里了;
麻烦的是需要考虑在多线程环境下维护队列的线程安全性,同时需要剔除取消的节点,考虑如何唤醒后继节点等… - 获取/释放资源方法: 因为不同的实现类对资源的定义不同,因此具体的获取和释放资源的方法 通过模版方法的方式交给子类来实现
AQS在具体的工作
- FIFO是其核心,维护队列,线程挂起、唤醒,取消节点移除、共享模式下的传播性维护等等,都是为了队列节点合理的获取资源,
维持其正确性和高效性,保证在多线程环境下资源合理的竞争,竞争者被合理的分配去获取资源。
以上个人理解,如有问题请指出,谢谢!