AbstractQueuedSynchronizer,简称为AQS,它是构建JDK中多个并发工具的基础。下图展示了JDK中使用AQS构建的并发工具。
可见,AQS在Java并发编程中是多么的重要。所以,我们有必要搞清楚其实现的原理。
一、AQS中的数据结构
在AQS类文件的注释中,作者已经给出了内部数据结构的说明。AQS里使用的是同步队列是CLH(Craig, Landin, and Hagersten)锁队列的一个变形,其中CLH锁通常用于自旋锁。CLH队列从节点的结构与节点等待机制两方面进行了改造:
①在结构上:引入了头结点和尾节点,他们分别指向队列的头和尾,尝试获取锁、入队列、释放锁等实现都与头尾节点相关,并且每个节点都引入前驱节点和后后续节点的引用;
②在等待机制上:由原来的自旋改成阻塞唤醒
AQS的核心原理:
当多个线程在竞争获取同步状态时,如果当前线程获取同步状态成功,则AQS会将当前线程标识为锁的持有者。如果当前线程如果获取同步状态失败时,AQS则会将当前线程以及等待状态等信息包装成一个节点,并将其添加到同步队列尾部等待锁的释放,同时阻塞当前线程。而当同步状态释放时,会唤醒后继结点,使其再次尝试获取同步状态。
下面来看一下结点的声明
static final class Node { /**竞争锁的两种模式**/ //共享模式 static final Node SHARED = new Node(); //排它模式 static final Node EXCLUSIVE = null; /**线程等待状态常量**/ //表明线程等待锁超时或已被取消。处于该状态后,状态不会再发生变化 static final int CANCELLED = 1; static final int SIGNAL = -1; static final int CONDITION = -2; static final int PROPAGATE = -3; //等待状态:取值范围为上面的几个值,初始值为0 volatile int waitStatus; //前驱节点 volatile Node prev; //后继节点 volatile Node next; //持有的线程(线程会被包装成节点) volatile Thread thread; //下一个在Condition条件上等待的节点。 //因为condition队列仅存在于排它模式下,所以当线程在condition上等待时,我们只需要一个简单的链表队列持有节点即可。之后他们可以被转移到竞争锁的同步队列中重新获取锁。 Node nextWaiter; }
Node结点是对每一个访问同步状态的线程的封装,其包含了需要同步的线程本身以及线程的状态,例如是否被阻塞,是否等待唤醒,是否已经被取消等。waitStatus则表示当前被封装成Node结点的等待状态,共有4种取值CANCELLED、SIGNAL、CONDITION、PROPAGATE。
- SIGNAL:值为-1。被标识为等待唤醒状态的后继结点,当其前继结点的线程释放了同步锁或被取消,将会通知该后继结点的线程执行。说白了,就是处于唤醒状态,只要前继结点释放锁,就会通知标识为SIGNAL状态的后继结点的线程执行。
- 初始状态:值为0,初始化状态。
- CANCELLED:值为1。在同步队列中等待的线程等待超时或被中断,需要从同步队列中取消该结点,其结点的waitStatus为CANCELLED,即结束状态,进入该状态后的结点将不会再变化。
- CONDITION:值为-2。与Condition相关,该标识的结点处于等待队列中,结点的线程等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
- PROPAGATE:值为-3。与共享模式相关,在共享模式中,该状态标识结点的线程处于可运行状态。
总结一下: 同步队列中waitStatus为SIGNAL(-1)的节点会被选为下一个获取锁的后继节点。 等待队列中waitStatus为CONDITION(-2)的节点会被在其它线程调用signal()后从等待队列转移到同步队列 等待队列中watiStatus为CANCELLED(1)的节点由于超时等待或被中断,而放弃获取锁。
同步队列中的节点如果想最终获得锁,则必须经过两个过程:被标记为CONDITION(-2)----->被标记为SIGNAL(-1)
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable { /** * 创建AQS实例,初始同步状态为0 */ protected AbstractQueuedSynchronizer() { } static final class Node {……} //头指针 private transient volatile Node head; //尾指针 private transient volatile Node tail; //同步状态 private volatile int state; }
二、获取/释放锁流程
1.排它模式获取锁acquire
public final void acquire(int arg) { //获取锁失败,则将线程封装成节点加入队列尾部,并中断当前线程。 if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } private Node addWaiter(Node mode) { //1.创建节点,并指定排它/模式模式 Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure //2.尝试用快速方式直接放到队尾。如果失败,则使用自旋方式添加。 Node pred = tail; if (pred != null) { //与旧尾节点连接(新节点prev指向旧尾节点) node.prev = pred; if (compareAndSetTail(pred, node)) { //与旧尾节点连接(旧尾节点next指向新节点) pred.next = node; return node; } } //3.通过自旋+CAS方式,将节点加入到队列尾部 enq(node); return node; } final boolean acquireQueued(final Node node, int arg) { boolean failed = true;// 标识是否成功获取同步状态 try { boolean interrupted = false;//标识等待过程中是否被中断过 //自旋操作:不停的判断自己是否排在head后面 for (;;) { final Node p = node.predecessor();//前驱 //如果前驱是head,说明自己正排在head后面,便有资格去尝试获取资源(head释放同步状态后唤醒自己,当然也可能被interrupt)。 if (p == head && tryAcquire(arg)) { setHead(node);//获取到同步状态后,就自己设置为head结点 p.next = null; // 便于GC回收以前的head结点 failed = false; return interrupted; } //执行到这里,说明:没有排在head后面,或者排在head后面但未获取到同步状态 //判断自己是否应该休息,如果是则通过park()进入waiting状态,直到被unpark。 //如果不可中断的情况下被中断了,那么会从park()中醒过来,发现拿不到同步状态,从而继续进入park。 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true;//如果等待过程中被中断过,将interrupted标记为true } } finally { if (failed) // 如果等待过程中没有成功获取资源(如超时,或可中断的情况下被中断了),那么取消结点在队列中的等待。 cancelAcquire(node); } } private static void selfInterrupt() { Thread.currentThread().interrupt(); }
如果获取同步状态成功,则直接返回。
如果获取同步状态失败。则加入到队列尾部。
- 首先尝试通过快速模式添加。
- 如果不成功,则通过CAS+自旋的方式添加。
成功加入到队列尾部后,结点即将进入等待状态。但不会立即进入等待状态,因为很可能此时head结点即将释放同步状态,自己立马就有资格获取到同步状态了。所以会先经历一段自旋时间,自旋时会不停的判断自己是否为正排在head结点后。
- 如果是排在head后则尝试获取同步状态,如果head恰好释放了同步状态,则会顺利拿到同步状态,之后将自身设为head结点。(运气好)
- 当然更常见的情况可能是发现自己没排在head后面,或者即使排在head后却一直拿不到同步状态,说明head可能一时半会不会立马释放同步状态,那要不干脆就休息一会吧!
通过判断,如果确实需要休息,则通过park进入waiting状态,直到被unpark。
2.排它模式释放锁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; } 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; 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. * * 要唤醒的线程是被后继节点所持有,后继节点通常来说就是下一个节点。 * 但如果后继节点被取消(CANCELED状态)或为空,则从尾节点向前遍历来查找真正非取消状态的后继。 */ //后继节点 Node s = node.next; //后继为空或为被取消状态(CANCELED) if (s == null || s.waitStatus > 0) { s = null;//若是被取消状态,则设置为null有助于GC //从尾部往前遍历查找最靠前的非取消状态的节点【为什么不从前往后查找?我的猜测:因为s==null,则s.next和s.prev都为null,无法遍历】 for (Node t = tail; t != null && t != node; t = t.prev) //只要等待状态<=0(-1,-2,-3,0),就可以唤醒 if (t.waitStatus <= 0) s = t; } if (s != null) //唤醒 LockSupport.unpark(s.thread); } public static void unpark(Thread thread) { if (thread != null) UNSAFE.unpark(thread); }
3.获取锁失败加入同步队列addWaiter
将竞争锁失败的线程加入到同步队列
public final void acquire(int arg) { //获取锁失败,则将线程封装成节点加入队列尾部,并中断当前线程。 if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } /** * Creates and enqueues node for current thread and given mode. * 将当前线程包装成节点加入等待队列,并指定模式(排它模式/共享模式) */ private Node addWaiter(Node mode) { //1.创建节点,并指定排它/模式模式 Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure //2.尝试用快速方式直接放到队尾。如果失败,则使用自旋方式添加。 Node pred = tail; if (pred != null) { //与旧尾节点连接(新节点prev指向旧尾节点) node.prev = pred; if (compareAndSetTail(pred, node)) { //与旧尾节点连接(旧尾节点next指向新节点) pred.next = node; return node; } } //3.通过自旋+CAS方式,将节点加入到队列尾部 enq(node); return node; } /** * Inserts node into queue, initializing if necessary. See picture above. * 将节点入队列,必要时会进行初始化。 */ private Node enq(final Node node) { //自旋+CAS方式将节点加到队列尾部 for (; ; ) { Node t = tail; //队列为空,则先初始化创建一个空节点,并将tail指针指向它 if (t == null) { // Must initialize if (compareAndSetHead(new Node())) tail = head; } else {//队列非空,则插入队尾 node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }
将竞争锁失败的线程入队列,大致分为以下3步:
①将该线程封装成节点,并指定当前所处于的模式(排它模式/共享模式)
②首先使用快捷方式尝试加入到队列:直接插入队列尾部。
③如果上述方式失败则使用自旋+CAS的方式插入队列尾部。
4.释放锁时唤醒后继unparkSuccessor
唤醒后继
head节点是获取同步状态成功的节点。首节点的线程在释放同步状态时,将会唤醒后继节点,而后继节点将会在获取同步状态成功时将自己设置为head节点。
/** * 唤醒后继节点 */ 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; 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. * * 要唤醒的线程是被后继节点所持有,后继节点通常来说就是下一个节点。 * 但如果后继节点被取消(CANCELED状态)或为空,则从尾节点向前遍历来查找真正非取消状态的后继。 */ //后继节点 Node s = node.next; //后继为空或为被取消状态(CANCELED) if (s == null || s.waitStatus > 0) { s = null;//若是被取消状态,则设置为null有助于GC //从尾部往前遍历查找最靠前的非取消状态的节点【为什么不从前往后查找?我的猜测:因为s==null,则s.next和s.prev都为null,无法遍历】 for (Node t = tail; t != null && t != node; t = t.prev) //只要等待状态<=0(-1,-2,-3,0),就可以唤醒 if (t.waitStatus <= 0) s = t; } if (s != null) //唤醒 LockSupport.unpark(s.thread); }
三、等待通知流程
5.等待await
public final void await() throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); //将当前线程包装成节点,加入到Condition等待队列(非同步队列)尾部 Node node = addConditionWaiter(); //释放当前线程的独占锁(不管重入几次,都把state释放为0) int savedState = fullyRelease(node); int interruptMode = 0; //如果当前节点没有在同步队列上,即还没有被signal,则将当前线程阻塞 while (!isOnSyncQueue(node)) { //阻塞当前线程 LockSupport.park(this); if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) break; } //加入到同步队列成功,且从等待模式退出未抛出中断异常(非等待超时或被中断),则将中断状态标记为重新中断 if (acquireQueued(node, savedState) && interruptMode != THROW_IE) interruptMode = REINTERRUPT; //从等待队列中断开该节点的连接 if (node.nextWaiter != null) // clean up if cancelled unlinkCancelledWaiters(); if (interruptMode != 0) reportInterruptAfterWait(interruptMode); }
调用await方法后,当前获取了锁的线程会被包装成节点加入到等待队列尾部,同时会释放锁,通知后继线程来获取锁。
6.通知signal
public final void signal() { if (!isHeldExclusively()) throw new IllegalMonitorStateException(); Node first = firstWaiter; if (first != null) doSignal(first); } private void doSignal(Node first) { do { if ((firstWaiter = first.nextWaiter) == null) lastWaiter = null; first.nextWaiter = null; } while (!transferForSignal(first) && (first = firstWaiter) != null); } //将被标记为CONDITION的节点转移到同步队列 final boolean transferForSignal(Node node) { if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) return false; Node p = enq(node); int ws = p.waitStatus; if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) LockSupport.unpark(node.thread); return true; }
线程调用signal后,会发通知将等待队列中的节点转移到同步队列中来获取锁。
总结:
1.AQS的实现原理?
2.CLH同步队列是怎么实现非公平和公平的?
参考: