一、写在前面
在上篇我们聊到AQS的原理,具体参见《J.U.C|AQS原理》。
这篇我们来给大家聊聊AQS中核心同步队列(CLH)。
二、什么是同步队列(CLH)
同步队列
一个FIFO双向队列,队列中每个节点等待前驱节点释放共享状态(锁)被唤醒就可以了。
AQS如何使用它?
AQS依赖它来完成同步状态的管理,当前线程如果获取同步状态失败时,AQS则会将当前线程已经等待状态等信息构造成一个节点(Node)并将其加入到CLH同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点唤醒(公平锁),使其再次尝试获取同步状态。
Node节点面貌?
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; /** 节点在等待队列中,线程在等待在Condition 上,其他线程对Condition调用singnal()方法后,该节点加入到同步队列中。 */ static final int CONDITION = -2; /** * 表示下一次共享式获取同步状态的时会被无条件的传播下去。 */ static final int PROPAGATE = -3; /**等待状态*/ volatile int waitStatus; /**前驱节点 */ volatile Node prev; /**后继节点*/ volatile Node next; /**获取同步状态的线程 */ volatile Thread thread; /**链接下一个等待状态 */ Node nextWaiter; // 下面一些方法就不贴了 }
CLH同步队列的结构图
这里是基于CAS(保证线程的安全)来设置尾节点的。
三、入列操作
如上图了解了同步队列的结构, 我们在分析其入列操作在简单不过。无非就是将tail(使用CAS保证原子操作)指向新节点,新节点的prev指向队列中最后一节点(旧的tail节点),原队列中最后一节点的next节点指向新节点以此来建立联系,来张图帮助大家理解。
源码
源码我们可以通过AQS中的以下两个方法来了解下
addWaiter方法
private Node addWaiter(Node mode) {// 以给定的模式来构建节点, mode有两种模式 // 共享式SHARED, 独占式EXCLUSIVE; 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; } } // 如果快速加入失败,则通过 anq方式入列 enq(node); return node; }
先通过addWaiter(Node node)方法尝试快速将该节点设置尾成尾节点,设置失败走enq(final Node node)方法
enq
private Node enq(final Node node) {// CAS自旋,直到加入队尾成功 for (;;) { Node t = tail; if (t == null) { // 如果队列为空,则必须先初始化CLH队列,新建一个空节点标识作为Hader节点,并将tail 指向它 if (compareAndSetHead(new Node())) tail = head; } else {// 正常流程,加入队列尾部 node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }
通过“自旋”也就是死循环的方式来保证该节点能顺利的加入到队列尾部,只有加入成功才会退出循环,否则会一直循序直到成功。
上述两个方法都是通过compareAndSetHead(new Node())方法来设置尾节点,以保证节点的添加的原子性(保证节点的添加的线程安全。)
四、出列操作
同步队列(CLH)遵循FIFO,首节点是获取同步状态的节点,首节点的线程释放同步状态后,将会唤醒它的后继节点(next),而后继节点将会在获取同步状态成功时将自己设置为首节点,这个过程非常简单。如下图
设置首节点是通过获取同步状态成功的线程来完成的(获取同步状态是通过CAS来完成),只能有一个线程能够获取到同步状态,因此设置头节点的操作并不需要CAS来保证,只需要将首节点设置为其原首节点的后继节点并断开原首节点的next(等待GC回收)应用即可。
五、总结
聊完后我们来总一下,同步队列就是一个FIFO双向对队列,其每个节点包含获取同步状态失败的线程应用、等待状态、前驱节点、后继节点、节点的属性类型以及名称描述。
其入列操作也就是利用CAS(保证线程安全)来设置尾节点,出列就很简单了直接将head指向新头节点并断开老头节点联系就可以了。