锁核心类AQS详解
背景
AbstractQueuedSynchronizer (抽象队列同步器,以下简称 AQS)出现在 JDK 1.5 中,由大师 Doug Lea 所创作。AQS 是很多同步器的基础框架,比如 ReentrantLock、CountDownLatch 和 Semaphore 等都是基于 AQS 实现的。除此之外,我们还可以基于 AQS,定制出我们所需要的同步器。
AQS 的使用方式通常都是通过内部类继承 AQS 实现同步功能,通过继承 AQS,作为一个内部辅助类实现同步原语,可以简化同步器的实现。屏蔽同步状态管理、线程的排队、等待与唤醒等底层操作。·例如ReenttrantLock的Sync内部类。
AQS简介
AQS是一个抽象类,类名为AbstractQueuedSynchronizer,抽象的都是一些公用的方法属性,其自身是没有实现任何同步接口的;AQS定义了同步器中获取锁和释放锁,目的来让自定义同步器组件来使用或重写; 纵观AQS的子类,绝大多数都是一个叫Sync的静态内部类来继承AQS类,通过重写AQS中的一些方法来实现自定义同步器;AQS定义了两种资源共享方式:EXCLUSIVE( 独占式:每次仅有一个Thread能执行 )、SHARED( 共享式:多个线程可同时执行 );
AQS 对资源的共享方式
Exclusive(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:
公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
Share(共享):多个线程可同时执行,如Semaphore、CountDownLatCh、 CyclicBarrier、ReadWriteLock ,
注意:ReentrantReadWriteLock 可以看成是组合式,因为ReentrantReadWriteLock也就是读写锁允许多个线程同时对某一资源进行读。
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在上层已经帮我们实现好了。
ReentrantLock: 使用了AQS的独占锁获取和释放,用state变量记录某个线程获取独占锁的次数,获取锁时+1,释放锁时-1,在获取时会校验线程是否可以获取锁。
Semaphore: 使用了AQS的共享锁获取和释放,用state变量作为计数器,只有在大于0时允许线程进入。获取锁时-1,释放锁时+1。
CountDownLatch: 使用了AQS的共享锁获取和释放,用state变量作为计数器,在初始化时指定。只要state还大于0,获取共享锁会因为失败而阻塞,直到计数器的值为0时,共享锁才允许获取,所有等待线程会被逐一唤醒。
1、CountDownLatch,简单大致意思为:A组线程等待另外B组线程,B组线程执行完了,A组线程才可以执行; state初始化假设为N,后续每countDown()一次,state会CAS减1。 等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。 2、ReentrantLock,简单大致意思为:独占式锁的类; state初始化为0,表示未锁定状态,然后每lock()时调用tryAcquire()使state加1, 其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁; 3、Semaphore,简单大致意思为:A、B、C、D线程同时争抢资源,目前卡槽大小为2,若A、B正在执行且未执行完,那么C、D线程在门外等着,一旦A、B有1个执行完了,那么C、D就会竞争看谁先执行; state初始值假设为N,后续每tryAcquire()一次,state会CAS减1,当state为0时其它线程处于等待状态, 直到state>0且<N后,进程又可以获取到锁进行各自操作了;
AQS维护了一个FIFO的CLH链表队列,且该队列不支持基于优先级的同步策略;
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。
AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。
1、队列模型: +------+ prev +------+ prev +------+ | | <---- | | <---- | | head | Node | next | Node | next | Node | tail | | ----> | | ----> | | +------+ +------+ +------+ 2、链表结构,在头尾结点中,需要特别指出的是头结点是一个空对象结点,无任何意义,即傀儡结点; 3、每一个Node结点都维护了一个指向前驱的指针和指向后驱的指针,结点与结点之间相互关联构成链表; 4、入队在尾,出队在头,出队后需要激活该出队结点的后继结点,若后继结点为空或后继结点waitStatus>0,则从队尾向前遍历取waitStatus<0的触发阻塞唤醒;
AQS数据结构
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。其中Sync queue,即同步队列,是双向链表,包括head结点和tail结点,head结点主要用作后续的调度。而Condition queue不是必须的,其是一个单向链表,只有当使用Condition时,才会存在此单向链表。并且可能会有多个Condition queue。
AQS大概实现思路
- 1、线程会首先尝试获取锁,如果失败,则将当前线程以及等待状态等信息包成一个Node节点加到同步队列里。
- 2、接着会不断自旋循环尝试获取锁(条件是当前节点为head的直接后继才会尝试),如果失败则会阻塞自己,直至被唤醒;
- 3、而当持有锁的线程释放锁时,会唤醒队列中的后继线程。
AQS底层使用了模板方法模式
同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用)。
AQS使用了模板方法模式,自定义同步器时需要重写下面几个AQS提供的模板方法:
isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。 tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。 tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。 tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。 tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。 默认情况下,每个方法都抛出 UnsupportedOperationException。 这些方法的实现必须是内部线程安全的,并且通常应该简短而不是阻塞。
AQS类中的其他方法都是final ,所以无法被其他类使用,只有这几个方法可以被其他类使用。 以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,
直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,
获取多少次就要释放多么次,这样才能保证state是能回到零态的
AQS源码分析
1、类的继承关系
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable
AbstractOwnableSynchronizer抽象类的源码如下:
public abstract class AbstractOwnableSynchronizer implements java.io.Serializable { // 版本序列号 private static final long serialVersionUID = 3737899427754241961L; // 构造方法 protected AbstractOwnableSynchronizer() { } // 独占模式下的线程 private transient Thread exclusiveOwnerThread; // 设置独占线程 protected final void setExclusiveOwnerThread(Thread thread) { exclusiveOwnerThread = thread; }
// 获取独占线程 protected final Thread getExclusiveOwnerThread() { return exclusiveOwnerThread; } }
2、类的属性及构造函数
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable { // 版本号 private static final long serialVersionUID = 7373984972572414691L; // 头结点 private transient volatile Node head; // 尾结点 private transient volatile Node tail; // 状态 private volatile int state; // 自旋时间 static final long spinForTimeoutThreshold = 1000L; // Unsafe类实例 private static final Unsafe unsafe = Unsafe.getUnsafe(); // state内存偏移地址 private static final long stateOffset; // head内存偏移地址 private static final long headOffset; // state内存偏移地址 private static final long tailOffset; // tail内存偏移地址 private static final long waitStatusOffset; // next内存偏移地址 private static final long nextOffset; // 静态初始化块 static { try { stateOffset = unsafe.objectFieldOffset (AbstractQueuedSynchronizer.class.getDeclaredField("state")); headOffset = unsafe.objectFieldOffset (AbstractQueuedSynchronizer.class.getDeclaredField("head")); tailOffset = unsafe.objectFieldOffset (AbstractQueuedSynchronizer.class.getDeclaredField("tail")); waitStatusOffset = unsafe.objectFieldOffset (Node.class.getDeclaredField("waitStatus")); nextOffset = unsafe.objectFieldOffset (Node.class.getDeclaredField("next")); } catch (Exception ex) { throw new Error(ex); } } }
AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改,状态信息通过procted类型的getState,setState,compareAndSetState进行操作。
private volatile int state;//共享变量,使用volatile修饰保证线程可见性 //返回同步状态的当前值 protected final int getState() { return state; } // 设置同步状态的值 protected final void setState(int newState) { state = newState; } //原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值) protected final boolean compareAndSetState(int expect, int update) { return unsafe.compareAndSwapInt(this, stateOffset, expect, update); }
此类构造方法为从抽象构造方法,供子类调用。
protected AbstractQueuedSynchronizer() { }
3、AQS类常用方法
1、protected boolean isHeldExclusively() // 需要被子类实现的方法,调用该方法的线程是否持有独占锁,一般用到了condition的时候才需要实现此方法 2、protected boolean tryAcquire(int arg) // 需要被子类实现的方法,独占方式尝试获取锁,获取锁成功后返回true,获取锁失败后返回false 3、protected boolean tryRelease(int arg) // 需要被子类实现的方法,独占方式尝试释放锁,释放锁成功后返回true,释放锁失败后返回false 4、protected int tryAcquireShared(int arg) // 需要被子类实现的方法,共享方式尝试获取锁,获取锁成功后返回正数1,获取锁失败后返回负数-1 5、protected boolean tryReleaseShared(int arg) // 需要被子类实现的方法,共享方式尝试释放锁,释放锁成功后返回正数1,释放锁失败后返回负数-1 6、final boolean acquireQueued(final Node node, int arg) // 对于进入队尾的结点,检测自己可以休息了,如果可以修改则进入SIGNAL状态且进入park()阻塞状态 7、private Node addWaiter(Node mode) // 添加结点到链表队尾 8、private Node enq(final Node node) // 如果addWaiter尝试添加队尾失败,则再次调用enq此方法自旋将结点加入队尾 9、private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) // 检测结点状态,如果可以休息的话则设置waitStatus=SIGNAL并调用LockSupport.park休息; 10、private void unparkSuccessor(Node node) // 释放锁时,该方法需要负责唤醒后继节点
4、AQS类的内部类 - Node类
每个线程被阻塞的线程都会被封装成一个Node结点,放入队列。每个节点包含了一个Thread类型的引用,并且每个节点都存在一个状态,具体状态如下。
-
CANCELLED,值为1,表示当前的线程被取消。 -
SIGNAL,值为-1,表示当前节点的后继节点包含的线程需要运行,需要进行unpark操作。 -
CONDITION,值为-2,表示当前节点在等待condition,也就是在condition queue中。 -
PROPAGATE,值为-3,表示当前场景下后续的acquireShared能够得以执行。 -
值为0,表示当前节点在sync queue中,等待着获取锁。
static final class Node { // 模式,分为共享与独占 // 共享模式 static final Node SHARED = new Node(); // 独占模式 static final Node EXCLUSIVE = null; // 结点状态 // CANCELLED,值为1,表示当前的线程被取消 // SIGNAL,值为-1,表示当前节点的后继节点包含的线程需要运行,也就是unpark // CONDITION,值为-2,表示当前节点在等待condition,也就是在condition队列中 // PROPAGATE,值为-3,表示当前场景下后续的acquireShared能够得以执行 // 值为0,表示当前节点在sync队列中,等待着获取锁 static final int CANCELLED = 1; static final int SIGNAL = -1; static final int CONDITION = -2; static final int PROPAGATE = -3; // 结点状态 volatile int waitStatus; // 前驱结点 volatile Node prev; // 后继结点 volatile Node next; // 结点所对应的线程 volatile Thread thread; // 下一个等待者 Node nextWaiter; // 结点是否在共享模式下等待 final boolean isShared() { return nextWaiter == SHARED; } // 获取前驱结点,若前驱结点为空,抛出异常 final Node predecessor() throws NullPointerException { // 保存前驱结点 Node p = prev; if (p == null) // 前驱结点为空,抛出异常 throw new NullPointerException(); else // 前驱结点不为空,返回 return p; } // 无参构造方法 Node() { // Used to establish initial head or SHARED marker } // 构造方法 Node(Thread thread, Node mode) { // Used by addWaiter this.nextWaiter = mode; this.thread = thread; } // 构造方法 Node(Thread thread, int waitStatus) { // Used by Condition this.waitStatus = waitStatus; this.thread = thread; } }
5、AQS类的内部类 - ConditionObject类
6、独占锁获取锁及释放锁核心类
伪代码如下:
1、获取独占锁: public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } acquire{ 如果尝试获取独占锁失败的话( 尝试获取独占锁的各种方式由AQS的子类实现 ), 那么就新增独占锁结点通过自旋操作加入到队列中,并且根据结点中的waitStatus来决定是否调用LockSupport.park进行休息 } 2、释放独占锁: public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; } release{ 如果尝试释放独占锁成功的话( 尝试释放独占锁的各种方式由AQS的子类实现 ), 那么取出头结点并根据结点waitStatus来决定是否有义务唤醒其后继结点 }
acquire方法 该方法以独占模式获取(资源),忽略中断,即线程在aquire过程中,中断此线程是无效的。
public final void acquire(int arg) { // 1.先调用tryAcquire方法,尝试获取独占锁,返回true则直接返回 if (tryAcquire(arg)) return; // 2. 调用addWaiter方法为当前线程创建一个节点node,并插入队列尾部,并标记为独占模式 Node node = addWaiter(Node.EXCLUSIVE); // 调用acquireQueued方法去获取锁,使线程在等待队列中获取资源,一直获取到资源才返回,如果在整个等待过程中被中断过,则返回true,否则返回false // 如果在线程等待过程中被中断过,他是不响应的。只是获取资源后再进行自我中断selfInterrupt(),则将中断补上
boolean interrupted = acquireQueued(node, arg); // 如果interrupted为true,则当前线程要发起中断请求 if (interrupted) { selfInterrupt(); } }
函数流程如下:

// 尝试去获取独占锁,立即返回。如果返回true表示获取锁成功。 protected boolean tryAcquire(int arg) { throw new UnsupportedOperationException(); }
如果子类想实现独占锁,则必须重写这个方法,否则抛出异常。这个方法的作用是当前线程尝试获取锁,如果获取到锁,就会返回true,并更改锁资源。没有获取到锁返回false。
注:这个方法是立即返回的,不会阻塞当前线程
tryAcquire尝试以独占的方式获取资源,如果获取成功,则直接返回true,否则直接返回false。该方法可以用于实现Lock中的tryLock()方法。该方法的默认实现是抛出UnsupportedOperationException,具体实现由自定义的扩展了AQS的同步类来实现。AQS在这里只负责定义了一个公共的方法框架。这里之所以没有定义成abstract,是因为独占模式下只用实现tryAcquire-tryRelease,而共享模式下只用实现tryAcquireShared-tryReleaseShared。如果都定义成abstract,那么每个模式也要去实现另一模式下的接口。
acquireQueued方法
- 通过for (;;)死循环,直到node节点的线程获取到锁,才跳出循环。
- 获取node节点的前一个节点p。
- 当前一个节点p时CLH队列头节点时,调用tryAcquire方法尝试去获取锁,如果获取成功,就将节点node设置成CLH队列头节点(相当于移除节点node和之前的节点)然后return返回。
注意:只有当node节点的前一个节点是队列头节点时,才会尝试获取锁,所以获取锁是有顺序的,按照添加到CLH队列时的顺序。- 调用shouldParkAfterFailedAcquire方法,来决定是否要阻塞当前线程。
- 调用parkAndCheckInterrupt方法,阻塞当前线程。
- 如果当前线程发生异常,非正常退出,那么会在finally模块中调用cancelAcquire(node)方法,取消当前节点状态。
注意:这里当尝试获取锁失败时,并没有立即阻塞当前线程,但是因为在for (;;)死循环里,会继续循环,方法不会返回。
/** * 想要获取锁的 acquire系列方法,都会这个方法来获取锁 * 循环通过tryAcquire方法不断去获取锁,如果没有获取成功, * 就有可能调用parkAndCheckInterrupt方法,让当前线程阻塞 * @param node 想要获取锁的节点 * @param arg * @return 返回true,表示在线程等待的过程中,线程被中断了 */ final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { // 表示线程在等待过程中,是否被中断了 boolean interrupted = false; // 通过死循环,直到node节点的线程获取到锁,才返回 for (;;) { // 获取node的前一个节点 final Node p = node.predecessor(); // 如果前一个节点是队列头head,并且尝试获取锁成功 // 那么当前线程就不需要阻塞等待,继续执行 if (p == head && tryAcquire(arg)) { // 将节点node设置为新的队列头 setHead(node); // help GC p.next = null; // 不需要调用cancelAcquire方法 failed = false; return interrupted; } // 当p节点的状态是Node.SIGNAL时,就会调用parkAndCheckInterrupt方法,阻塞node线程 // node线程被阻塞,有两种方式唤醒, // 1.是在unparkSuccessor(Node node)方法,会唤醒被阻塞的node线程,返回false // 2.node线程被调用了interrupt方法,线程被唤醒,返回true // 在这里只是简单地将interrupted = true,没有跳出for的死循环,继续尝试获取锁 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { // failed为true,表示发生异常,非正常退出 // 则将node节点的状态设置成CANCELLED,表示node节点所在线程已取消,不需要唤醒了。 if (failed) cancelAcquire(node); } }
addWaiter(Node)
addWaiter通过传入不同的模式来创建新的结点尝试加入到队列尾部,如果由于并发导致添加结点到队尾失败的话那么就进入自旋将结点加入队尾;
/** * 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) { // 按照给定的mode模式创建新的结点,模式有两种:Node.EXCLUSIVE独占模式、Node.SHARED共享模式; Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure Node pred = tail; // 将先队尾结点赋值给临时变量 if (pred != null) { // 如果pred不为空,说明该队列已经有结点了 node.prev = pred; if (compareAndSetTail(pred, node)) { // 通过CAS尝试将node结点设置为队尾结点 pred.next = node; return node; } } // 执行到此,说明队尾没有元素,则进入自旋首先设置头结点,然后将此新建结点添加到队尾 enq(node); // 进入自旋添加node结点 return node; }
enq(Node)
enq通过自旋这种死循环的操作方式,来确保结点正确的添加到队列尾部,通过CAS操作如果头部为空则添加傀儡空结点,然后在循环添加队尾结点;
/** * Inserts node into queue, initializing if necessary. See picture above. * @param node the node to insert * @return node's predecessor */ private Node enq(final Node node) { for (;;) { // 自旋的死循环操作方式 Node t = tail; // 因为是自旋方式,首次链表队列tail肯定为空,但是后续链表有数据后就不会为空了 if (t == null) { // Must initialize if (compareAndSetHead(new Node())) // 队列为空时,则创建一个空对象结点作为头结点,无意思,可认为傀儡结点 tail = head; // 空队列的话,头尾都指向同一个对象 } else { // 进入 else 方法里面,说明链表队列已经有结点了 node.prev = t; // 因为存在并发操作,通过CAS尝试将新加入的node结点设置为队尾结点 if (compareAndSetTail(t, node)) { // 如果node设置队尾结点成功,则将之前的旧的对象尾结点t的后继结点指向node,node的前驱结点也设置为t t.next = node; return t; } } // 如果执行到这里,说明上述两个CAS操作任何一个失败的话,该方法是不会放弃的,因为是自旋操作,再次循环继续入队 } }
/** * 通过 CAS + 自旋的方式插入节点到队尾 */ private Node enq(final Node node) { for (;;) { Node t = tail; if (t == null) { // Must initialize // 设置头结点,初始情况下,头结点是一个空节点 if (compareAndSetHead(new Node())) tail = head; } else { /* * 将节点插入队列尾部。这里是先将新节点的前驱设为尾节点,之后在尝试将新节点设为尾节 * 点,最后再将原尾节点的后继节点指向新的尾节点。除了这种方式,我们还先设置尾节点, * 之后再设置前驱和后继,即: * * if (compareAndSetTail(t, node)) { * node.prev = t; * t.next = node; * } * * 但但如果是这样做,会导致一个问题,即短时内,队列结构会遭到破坏。考虑这种情况, * 某个线程在调用 compareAndSetTail(t, node)成功后,该线程被 CPU 切换了。此时 * 设置前驱和后继的代码还没带的及执行,但尾节点指针却设置成功,导致队列结构短时内会 * 出现如下情况: * * +------+ prev +-----+ +-----+ * head | | <---- | | | | tail * | | ----> | | | | * +------+ next +-----+ +-----+ * * tail 节点完全脱离了队列,这样导致一些队列遍历代码出错。如果先设置 * 前驱,在设置尾节点。及时线程被切换,队列结构短时可能如下: * * +------+ prev +-----+ prev +-----+ * head | | <---- | | <---- | | tail * | | ----> | | | | * +------+ next +-----+ +-----+ * * 这样并不会影响从后向前遍历,不会导致遍历逻辑出错。 * node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } } /** * 同步队列中的线程在此方法中以循环尝试获取同步状态,在有限次的尝试后, * 若仍未获取锁,线程将会被阻塞,直至被前驱节点的线程唤醒。 */ final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; // 循环获取同步状态 for (;;) { final Node p = node.predecessor(); /* * 前驱节点如果是头结点,表明前驱节点已经获取了同步状态。前驱节点释放同步状态后, * 在不出异常的情况下, tryAcquire(arg) 应返回 true。此时节点就成功获取了同 * 步状态,并将自己设为头节点,原头节点出队。 */ if (p == head && tryAcquire(arg)) { // 成功获取同步状态,设置自己为头节点 setHead(node); p.next = null; // help GC failed = false; return interrupted; } /* * 如果获取同步状态失败,则根据条件判断是否应该阻塞自己。 * 如果不阻塞,CPU 就会处于忙等状态,这样会浪费 CPU 资源 */ if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { /* * 如果在获取同步状态中出现异常,failed = true,cancelAcquire 方法会被执行。 * tryAcquire 需同步组件开发者覆写,难免不了会出现异常。 */ if (failed) cancelAcquire(node); } } /** 设置头节点 */ private void setHead(Node node) { // 仅有一个线程可以成功获取同步状态,所以这里不需要进行同步控制 head = node; node.thread = null; node.prev = null; } /** * 该方法主要用途是,当线程在获取同步状态失败时,根据前驱节点的等待状态,决定后续的动作。比如前驱 * 节点等待状态为 SIGNAL,表明当前节点线程应该被阻塞住了。不能老是尝试,避免 CPU 忙等。 * ————————————————————————————————————————————————————————————————— * | 前驱节点等待状态 | 相应动作 | * ————————————————————————————————————————————————————————————————— * | SIGNAL | 阻塞 | * | CANCELLED | 向前遍历, 移除前面所有为该状态的节点 | * | waitStatus < 0 | 将前驱节点状态设为 SIGNAL, 并再次尝试获取同步状态 | * ————————————————————————————————————————————————————————————————— */ private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; /* * 前驱节点等待状态为 SIGNAL,表示当前线程应该被阻塞。 * 线程阻塞后,会在前驱节点释放同步状态后被前驱节点线程唤醒 */ if (ws == Node.SIGNAL) return true; /* * 前驱节点等待状态为 CANCELLED,则以前驱节点为起点向前遍历, * 移除其他等待状态为 CANCELLED 的节点。 */ if (ws > 0) { do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { /* * 等待状态为 0 或 PROPAGATE,设置前驱节点等待状态为 SIGNAL, * 并再次尝试获取同步状态。 */ compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; } private final boolean parkAndCheckInterrupt() { // 调用 LockSupport.park 阻塞自己 LockSupport.park(this); return Thread.interrupted(); } /** * 取消获取同步状态 */ private void cancelAcquire(Node node) { if (node == null) return; node.thread = null; // 前驱节点等待状态为 CANCELLED,则向前遍历并移除其他为该状态的节点 Node pred = node.prev; while (pred.waitStatus > 0) node.prev = pred = pred.prev; // 记录 pred 的后继节点,后面会用到 Node predNext = pred.next; // 将当前节点等待状态设为 CANCELLED node.waitStatus = Node.CANCELLED; /* * 如果当前节点是尾节点,则通过 CAS 设置前驱节点 prev 为尾节点。设置成功后,再利用 CAS 将 * prev 的 next 引用置空,断开与后继节点的联系,完成清理工作。 */ if (node == tail && compareAndSetTail(node, pred)) { /* * 执行到这里,表明 pred 节点被成功设为了尾节点,这里通过 CAS 将 pred 节点的后继节点 * 设为 null。注意这里的 CAS 即使失败了,也没关系。失败了,表明 pred 的后继节点更新 * 了。pred 此时已经是尾节点了,若后继节点被更新,则是有新节点入队了。这种情况下,CAS * 会失败,但失败不会影响同步队列的结构。 */ compareAndSetNext(pred, predNext, null); } else { 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) /* * 这里使用 CAS 设置 pred 的 next,表明多个线程同时在取消,这里存在竞争。 * 不过此处没针对 compareAndSetNext 方法失败后做一些处理,表明即使失败了也 * 没关系。实际上,多个线程同时设置 pred 的 next 引用时,只要有一个能设置成 * 功即可。 */ compareAndSetNext(pred, predNext, next); } else { /* * 唤醒后继节点对应的线程。这里简单讲一下为什么要唤醒后继线程,考虑下面一种情况: * head node1 node2 tail * ws=0 ws=1 ws=-1 ws=0 * +------+ prev +-----+ prev +-----+ prev +-----+ * | | <---- | | <---- | | <---- | | * | | ----> | | ----> | | ----> | | * +------+ next +-----+ next +-----+ next +-----+ * * 头结点初始状态为 0,node1、node2 和 tail 节点依次入队。node1 自旋过程中调用 * tryAcquire 出现异常,进入 cancelAcquire。head 节点此时等待状态仍然是 0,它 * 会认为后继节点还在运行中,所它在释放同步状态后,不会去唤醒后继等待状态为非取消的 * 节点 node2。如果 node1 再不唤醒 node2 的线程,该线程面临无法被唤醒的情况。此 * 时,整个同步队列就回全部阻塞住。 */ unparkSuccessor(node); } node.next = node; // help GC } } private void unparkSuccessor(Node node) { int ws = node.waitStatus; /* * 通过 CAS 将等待状态设为 0,让后继节点线程多一次 * 尝试获取同步状态的机会 */ if (ws < 0) compareAndSetWaitStatus(node, ws, 0); Node s = node.next; if (s == null || s.waitStatus > 0) { s = null; /* * 这里如果 s == null 处理,是不是表明 node 是尾节点?答案是不一定。原因之前在分析 * enq 方法时说过。这里再啰嗦一遍,新节点入队时,队列瞬时结构可能如下: * node1 node2 * +------+ prev +-----+ prev +-----+ * head | | <---- | | <---- | | tail * | | ----> | | | | * +------+ next +-----+ +-----+ * * node2 节点为新入队节点,此时 tail 已经指向了它,但 node1 后继引用还未设置。 * 这里 node1 就是 node 参数,s = node1.next = null,但此时 node1 并不是尾 * 节点。所以这里不能从前向后遍历同步队列,应该从后向前。 */ for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } if (s != null) // 唤醒 node 的后继节点线程 LockSupport.unpark(s.thread); }
释放独占锁的方法
release方法 release尝试释放锁,并且有义务移除CANCELLED状态的结点,还有义务唤醒后继结点继续运行获取锁资源;
相对于获取同步状态,释放同步状态的过程则要简单的多,这里简单罗列一下步骤:
- 调用 tryRelease(arg) 尝试释放同步状态
- 根据条件判断是否应该唤醒后继线程
// 在独占锁模式下,释放锁的操作 public final boolean release(int arg) { // 调用tryRelease方法,尝试去释放锁,由子类具体实现 if (tryRelease(arg)) { Node h = head; // 如果队列头节点的状态不是0,那么队列中就可能存在需要唤醒的等待节点。 // 还记得我们在acquireQueued(final Node node, int arg)获取锁的方法中,如果节点node没有获取到锁, // 那么我们会将节点node的前一个节点状态设置为Node.SIGNAL,然后调用parkAndCheckInterrupt方法 // 将节点node所在线程阻塞。 // 在这里就是通过unparkSuccessor方法,进而调用LockSupport.unpark(s.thread)方法,唤醒被阻塞的线程 if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }
流程如下:
- 调用tryRelease方法释放锁资源,返回true表示锁资源完全释放了,返回false表示还持有锁资源。
- 如果锁资源完全被释放了,就要唤醒等待锁资源的线程。调用unparkSuccessor方法唤醒一个等待线程
注:CLH队列头节点h为null,表示队列为空,没有节点。节点h的状态是0,表示没有CLH队列中没有被阻塞的线程。
tryRelease主要通过CAS操作对state锁资源进行减1操作;
// 尝试去释放当前线程持有的独占锁,立即返回。如果返回true表示释放锁成功 protected boolean tryRelease(int arg) { throw new UnsupportedOperationException(); }
如果子类想实现独占锁,则必须重写这个方法,否则抛出异常。作用是释放当前线程持有的锁,返回true表示已经完全释放锁资源,返回false,表示还持有锁资源。
对于独占锁来说,同一时间只能有一个线程持有这个锁,但是这个线程可以重复地获取锁,因为被锁住的模块,再次进入另一个被这个锁锁住的模块,是允许的。这个就做可重入性,所以对于可重入的锁释放操作,也需要多次。
unparkSuccessor方法
主要是踢出CANCELLED状态结点,然后唤醒后继结点;但是这个唤醒的后继结点为空的话,那么则从队尾一直向前循环查找小于等于零状态的结点并调用unpark唤醒;/** * Wakes up node's successor, if one exists. * * @param node the node */ 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. */ // 该node一般都是传入head进来,也就是说,需要释放头结点,也就是当前结点需要释放锁操作,顺便唤醒后继结点 int ws = node.waitStatus; if (ws < 0) // 若结点状态值小于0,则归零处理,通过CAS归零,允许失败,但是不管怎么着,仍然要往下走去唤醒后继结点 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. */ Node s = node.next; // 取出后继结点,这个时候一般都是Head后面的一个结点,所以一般都是老二 if (s == null || s.waitStatus > 0) { // 若后继结点为空或者后继结点已经处于CANCELLED状态的话 s = null; // 那么从队尾向前遍历,直到找到一个小于等于0的结点 // 这里为什么要从队尾向前寻找? // * 因为在这个队列中,任何一个结点都有可能被中断,只是有可能,并不代表绝对的,但有一点是确定的, // * 被中断的结点会将结点的状态设置为CANCELLED状态,标识这个结点在将来的某个时刻会被踢出; // * 踢出队列的规则很简单,就是该结点的前驱结点不会指向它,而是会指向它的后面的一个非CANCELLED状态的结点; // * 而这个将被踢出的结点,它的next指针将会指向它自己; // * 所以设想一下,如果我们从head往后找,一旦发现这么一个处于CANCELLED状态的结点,那么for循环岂不是就是死循环了; // * 但是所有的这些结点当中,它们的prev前驱结点还是没有被谁动过,所以从tail结点向前遍历最稳妥 for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } if (s != null) LockSupport.unpark(s.thread); // 唤醒线程 }
7、共享锁获取锁及释放锁核心类
3、获取共享锁: public final void acquireShared(int arg) { if (tryAcquireShared(arg) < 0) doAcquireShared(arg); } acquireShared{ 如果尝试获取共享锁失败的话( 尝试获取共享锁的各种方式由AQS的子类实现 ), 那么新增共享锁结点通过自旋操作加入到队尾中,并且根据结点中的waitStatus来决定是否调用LockSupport.park进行休息 } 4、释放共享锁: public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { doReleaseShared(); return true; } return false; } releaseShared{ 如果尝试释放共享锁失败的话( 尝试释放共享锁的各种方式由AQS的子类实现 ), 那么通过自旋操作唤完成阻塞线程的唤起操作 }
doAcquireShared方法
/** * 获取共享锁,获取失败,则会阻塞当前线程,直到获取共享锁返回 * @param arg the acquire argument */ private void doAcquireShared(int arg) { // 为当前线程创建共享锁节点node final Node node = addWaiter(Node.SHARED); boolean failed = true; try { boolean interrupted = false; for (;;) { final Node p = node.predecessor(); // 如果节点node前一个节点是同步队列头节点。就会调用tryAcquireShared方法尝试获取共享锁 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; } } // 如果节点p的状态是Node.SIGNAL,就是调用parkAndCheckInterrupt方法阻塞当前线程 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { // failed为true,表示发生异常, // 则将node节点的状态设置成CANCELLED,表示node节点所在线程已取消,不需要唤醒了 if (failed) cancelAcquire(node); } }
这个方法与独占锁的acquireQueued方法相比较,不同的有三点:
- doAcquireShared方法,调用addWaiter(Node.SHARED)方法,为当前线程创建一个共享模式的节点node。而acquireQueued方法是由外部传递来的。
- doAcquireShared方法没有返回值,acquireQueued方法会返回布尔类型的值,是当前线程中断标志位值
- 最大的区别是重新设置CLH队列头的方法不一样。doAcquireShared方法调用setHeadAndPropagate方法,而acquireQueued方法调用setHead方法。
// 重新设置CLH队列头,如果CLH队列头的下一个节点为null或者共享模式, // 那么就要唤醒共享锁上等待的线程 private void setHeadAndPropagate(Node node, int propagate) { Node h = head; // 设置新的同步队列头head setHead(node); // 如果propagate大于0, if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) { // 获取新的CLH队列头的下一个节点s Node s = node.next; // 如果节点s是空或者共享模式节点,那么就要唤醒共享锁上等待的线程 if (s == null || s.isShared()) doReleaseShared(); } }
释放共享锁的方法
releaseShared方法
// 释放共享锁 public final boolean releaseShared(int arg) { // 尝试释放共享锁 if (tryReleaseShared(arg)) { // 唤醒等待共享锁的线程 doReleaseShared(); return true; } return false; }
tryReleaseShared方法
// 尝试去释放共享锁 protected boolean tryReleaseShared(int arg) { throw new UnsupportedOperationException(); }
如果子类想实现共享锁,则必须重写这个方法,否则抛出异常。作用是释放当前线程持有的锁,返回true表示已经完全释放锁资源,返回false,表示还持有锁资源。
doReleaseShared方法
// 会唤醒等待共享锁的线程 private void doReleaseShared() { for (;;) { // 将同步队列头赋值给节点h Node h = head; // 如果节点h不为null,且不等于同步队列尾 if (h != null && h != tail) { // 得到节点h的状态 int ws = h.waitStatus; // 如果状态是Node.SIGNAL,就要唤醒节点h后继节点的线程 if (ws == Node.SIGNAL) { // 将节点h的状态设置成0,如果设置失败,就继续循环,再试一次。 if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) continue; // loop to recheck cases // 唤醒节点h后继节点的线程 unparkSuccessor(h); } // 如果节点h的状态是0,就设置ws的状态是PROPAGATE。 else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; // loop on failed CAS } // 如果同步队列头head节点发生改变,继续循环, // 如果没有改变,就跳出循环 if (h == head) break; } }
总结
使用AQS类来实现独占锁和共享锁:
- 内部有一个CLH队列,用来记录所有等待锁的线程
- 通过 acquire系列方法用来获取独占锁,获取失败,则阻塞当前线程
- 通过release方法用来释放独占锁,释放成功,则会唤醒一个等待独占锁的线程。
- 通过acquireShared系列方法用来获取共享锁。
- 通过releaseShared方法用来释放共享锁。
- 通过Condition来实现线程之间相互等待的。
:A组线程等待另外B组线程,B组线程执行完了,A组线程才可以执行;
state初始化假设为N,后续每countDown()一次,state会CAS减1。
等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。
一些思考
插入节点时的代码顺序
addWaiter和enq方法中新增一个节点时为什么要先将新节点的prev置为tail再尝试CAS,而不是CAS成功后来构造节点之间的双向链接?
这是因为,双向链表目前没有基于CAS原子插入的手段,如果我们将node.prev = t和t.next = node(t为方法执行时读到的tail,引用封闭在栈上)
放到compareAndSetTail(t, node)成功后执行,如下所示:
if (compareAndSetTail(t, node)) {
node.prev = t;
t.next = node;
return t;
}
会导致这一瞬间的tail也就是t的prev为null,这就使得这一瞬间队列处于一种不一致的中间状态。
唤醒节点时为什么从tail向前遍历
unparkSuccessor方法中为什么唤醒后继节点时要从tail向前查找最接近node的非取消节点,而不是直接从node向后找到第一个后break掉?
在上面的代码注释中已经提及到这一点:
如果读到s == null,不代表node就为tail。
考虑如下场景:
- node某时刻为tail
- 有新线程通过addWaiter中的if分支或者enq方法添加自己
- compareAndSetTail成功
- 此时这里的Node s = node.next读出来s == null,但事实上node已经不是tail,它有后继了!
unparkSuccessor有新线程争锁是否存在漏洞
unparkSuccessor方法在被release调用时是否存在这样的一个漏洞?
时刻1: node -> tail && tail.waitStatus == Node.CANCELLED (node的下一个节点为tail,并且tail处于取消状态)
时刻2: unparkSuccessor读到s.waitStatus > 0
时刻3: unparkSuccessor从tail开始遍历
时刻4: tail节点对应线程执行cancelAcquire方法中的if (node == tail && compareAndSetTail(node, pred)) 返回true,
此时tail变为pred(也就是node)
时刻5: 有新线程进队列tail变为新节点
时刻6: unparkSuccessor没有发现需要唤醒的节点
最终新节点阻塞并且前驱节点结束调用,新节点再也无法被unpark
这种情况不会发生,确实可能出现从tail向前扫描,没有读到新入队的节点,但别忘了acquireQueued的思想就是不断循环检测是否能够独占获取锁,
否则再进行判断是否要阻塞自己,而release的第一步就是tryRelease,它的语义为true表示完全释放独占锁,完全释放之后才会执行后面的逻辑,也就是unpark后继线程。在这种情况下,新入队的线程应当能获取到锁。
如果没有获取锁,则必然是在覆盖tryAcquire/tryRelease的实现有问题,导致前驱节点成功释放了独占锁,后继节点获取独占锁仍然失败。也就是说AQS框架的可靠性还在
某些程度上依赖于具体子类的实现,子类实现如果有bug,那AQS再精巧也扛不住。
AQS如何保证队列活跃
AQS如何保证在节点释放的同时又有新节点入队的情况下,不出现原持锁线程释放锁,后继线程被自己阻塞死的情况,保持同步队列的活跃?
回答这个问题,需要理解shouldParkAfterFailedAcquire和unparkSuccessor这两个方法。
以独占锁为例,后继争用线程阻塞自己的情况是读到前驱节点的等待状态为SIGNAL,只要不是这种情况都会再试着去争取锁。
假设后继线程读到了前驱状态为SIGNAL,说明之前在tryAcquire的时候,前驱持锁线程还没有tryRelease完全释放掉独占锁。
此时如果前驱线程完全释放掉了独占锁,则在unparkSuccessor中还没执行完置waitStatus为0的操作,也就是还没执行到下面唤醒后继线程的代码,否则后继线程会再去争取锁。
那么就算后继争用线程此时把自己阻塞了,也一定会马上被前驱线程唤醒。
那么是否可能持锁线程执行唤醒后继线程的逻辑时,后继线程读到前驱等待状态为SIGNAL把自己给阻塞,再也无法苏醒呢?
这个问题在上面的问题3中已经有答案了,确实可能在扫描后继需要唤醒线程时读不到新来的线程,但只要tryRelease语义实现正确,在true时表示完全释放独占锁,
则后继线程理应能够tryAcquire成功,shouldParkAfterFailedAcquire在读到前驱状态不为SIGNAL会给当前线程再一次获取锁的机会的。
别看AQS代码写的有些复杂,状态有些多,还真的就是没毛病,各种情况都能覆盖。
AQS如何防止内存泄露
AQS维护了一个FIFO队列,它是如何保证在运行期间不发生内存泄露的?
AQS在无竞争条件下,甚至都不会new出head和tail节点。
线程成功获取锁时设置head节点的方法为setHead,由于头节点的thread并不重要,此时会置node的thread和prev为null,
完了之后还会置原先head也就是线程对应node的前驱的next为null,从而实现队首元素的安全移出。
而在取消节点时,也会令node.thread = null,在node不为tail的情况下,会使node.next = node(之所以这样也是为了isOnSyncQueue实现更加简洁)
问题一:PROPAGATE 状态用在哪里,以及怎样向后传播唤醒动作的?
答:PROPAGATE 状态用在 setHeadAndPropagate。当头节点状态被设为 PROPAGATE 后,后继节点成为新的头结点后。若 propagate > 0 条件不成立,则根据条件h.waitStatus < 0成立与否,来决定是否唤醒后继节点,即向后传播唤醒动作。
问题二:引入 PROPAGATE 状态是为了解决什么问题?
答:引入 PROPAGATE 状态是为了解决并发释放信号量所导致部分请求信号量的线程无法被唤醒的问题。
浙公网安备 33010602011771号