并发编程之CLH同步队列出队入队详解
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
并发编程之CLH同步队列出队⼊队详解
本章重点讲解内容如下:
1、什么是CLH同步队列
2、为什么需要CLH同步队列
3、CLH同步队列原理(即队列如何⼊队、出队)
⼀什么是CLH队列
AbstractQueuedSynchronizer类⽂件开头,作者Doug Lea⼀⼤篇幅来介绍CLH队列,⼤意如下:
CLH队列是⼀个FIFO的双向链表:由head、tail、node中间节点组成,每个Node节点包含:thread、waitStatus、next、pre属性
当线程获取同步状态失败后,会将当前线程构造成⼀个Node节点插⼊链表(如果第⼀次插⼊会初始化head节点为虚拟节点),插⼊链表都是尾部插⼊并且setTail为当前节点,同时会阻塞当前线程(调⽤LockSupport.park⽅法)。
当线程释放同步状态后,会唤醒当前节点的next节点,next节点会抢占同步资源,抢占失败后重新阻塞,成功后next节点会重新setHead为当前线程的节点,将之前的head废弃。
⼆为什么需要CLH队列
是为了减少多线程抢占资源造成不必要的cpu上下⽂切换开销。
通过看AQS源码我们知道抢占同步器状态是调⽤pareAndSwapInt⽅法,其实底层就是调⽤的jvm
的cas函数。
当多个线程同时在cas的时候,最多只能有⼀个抢占成功,其余的都在⾃旋,这样就造成了不必要的cpu开销。
若引⼊CLH队列队列,⾄于pre执⾏完毕,才唤醒next节点,这样最多只有next节点和新进⼊的线程抢占cpu资源,其余的线程都是阻塞状态,极⼤的减少了不必要的cpu开
销。
三 CLH队列原理(如何⼊队、出队)
1)⼊队
⼊队代码如下:
1//获取锁
2public final void acquire(int arg) {
3//tryAcquire尝试获取锁,Semaphore、coutDownLatch等各个⼯具类实现不⼀致
4if (!tryAcquire(arg) &&
5//acquireQueued:tryAcquire成功就setHead为当前节点,失败则阻塞当前线程
6//addWaiter加⼊同步等待队列
7 acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
8 selfInterrupt();
9 }
10//加⼊等待队列
11private Node addWaiter(Node mode) {
12 Node node = new Node(Thread.currentThread(), mode);
13// Try the fast path of enq; backup to full enq on failure
14// ⾮⾸次插⼊,可直接setTail
15// 设置⽼的tail为当天tail的pre节点
16 Node pred = tail;
17if (pred != null) {
18 node.prev = pred;
19if (compareAndSetTail(pred, node)) {
20 pred.next = node;
21return node;
22 }
23 }
24//⾸次插⼊,需要创建虚拟的head节点
25 enq(node);
26return node;
27 }
28private Node enq(final Node node) {
29for (;;) {
30 Node t = tail;
31// 如果 tail 是 null,就创建⼀个虚拟节点,同时指向 head 和 tail,称为初始化。
32if (t == null) { // Must initialize
33if (compareAndSetHead(new Node()))
34 tail = head;
35 } else {// 如果不是 null
36// 和上个⽅法逻辑⼀样,将新节点追加到 tail 节点后⾯,并更新队列的 tail 为新节点。
37// 只不过这⾥是死循环的,失败了还可以再来。
38 node.prev = t;
39if (compareAndSetTail(t, node)) {
40 t.next = node;
41return t;
42 }
43 }
44 }
45 }
View Code
阻塞当前线程代码如下:
1// 这⾥返回的节点是新创建的节点,arg 是请求的数量
2final boolean acquireQueued(final Node node, int arg) {
3boolean failed = true;
4try {
5boolean interrupted = false;
6for (;;) {
7// 找上⼀个节点
8final Node p = node.predecessor();
9// 如果上⼀个节点是 head ,就尝试获取锁
10// 如果获取成功,就将当前节点设置为 head,注意 head 节点是永远不会唤醒的。
11if (p == head && tryAcquire(arg)) {
12 setHead(node);
13 p.next = null; // help GC
14 failed = false;
15return interrupted;
16 }
17// 在获取锁失败后,就需要阻塞了。
18// shouldParkAfterFailedAcquire ---> 检查上⼀个节点的状态,如果是 SIGNAL 就阻塞,否则就改成 SIGNAL。
19if (shouldParkAfterFailedAcquire(p, node) &&
20//parkAndCheckInterrupt就是调⽤park⽅法阻塞当前线程,等待被唤起后,重新进⼊当前⾃旋操作,可重新获取锁
21 parkAndCheckInterrupt())
22 interrupted = true;
23 }
24 } finally {
25if (failed)
26 cancelAcquire(node);
27 }
28 }
View Code
2)出队
1//释放当前线程
2public final boolean release(int arg) {
3//实际操作就是cas把AQS的state状态-1
4if (tryRelease(arg)) {
5 Node h = head;
6if (h != null && h.waitStatus != 0)
7//核⼼⽅法,见后⾯详解
8 unparkSuccessor(h);
9return true;
10 }
11return false;
12 }
13
14//释放锁核⼼⽅法
15private void unparkSuccessor(Node node) {
16int ws = node.waitStatus;
17if (ws < 0)
18// 将 head 节点的 ws 改成 0,清除信号。
表⽰,他已经释放过了。
不能重复释放。
19 compareAndSetWaitStatus(node, ws, 0);
20
21 Node s = node.next;
22// 如果 next 是 null,或者 next 被取消了。
就从 tail 开始向上找节点。
23if (s == null || s.waitStatus > 0) {
24 s = null;
25// 从尾部开始,向前寻找未被取消的节点,直到这个节点是 null,或者是 head。
26// 也就是说,如果 head 的 next 是 null,那么就从尾部开始寻找,直到不是 null 为⽌,找到这个 head 就不管了。
27// 如果是 head 的 next 不是 null,但是被取消了,那这个节点也会被略过。
28for (Node t = tail; t != null && t != node; t = t.prev)
29if (t.waitStatus <= 0)
30 s = t;
31 }
32// 唤醒 head.next 这个节点。
33// 通常这个节点是 head 的 next。
34// 但如果 head.next 被取消了,就会从尾部开始找。
35if (s != null)
36 LockSupport.unpark(s.thread);
37 }
View Code
3)如何联动起来(即如何按FIFO顺序唤醒队列)
调⽤Lock.release()⽅法会触发unparkSuccessor()--> LockSupport.unpark(node.next)
唤醒next节点,next节点会继续在acquireQueued()⽅法⾥⾃旋,若tryAcquire()成功,执⾏setHead(node)⽅法,把CLH队列head设置为当前节点,然后等待当前节点执⾏逻辑完毕再次调⽤release⽅法,重复执⾏上述逻辑。
若获取锁失败,继续进⼊阻塞状态,等待下⼀次被唤醒。