本章重点讲解内容如下:
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源码我们知道抢占同步器状态是调用UnSafe.compareAndSwapInt方法,其实底层就是调用的jvm的cas函数。当多个线程同时在cas的时候,最多只能有一个抢占成功,其余的都在自旋,这样就造成了不必要的cpu开销。
若引入CLH队列队列,至于pre执行完毕,才唤醒next节点,这样最多只有next节点和新进入的线程抢占cpu资源,其余的线程都是阻塞状态,极大的减少了不必要的cpu开销。
三 CLH队列原理(如何入队、出队)
1)入队
入队代码如下:
1 //获取锁 2 public final void acquire(int arg) { 3 //tryAcquire尝试获取锁,Semaphore、coutDownLatch等各个工具类实现不一致 4 if (!tryAcquire(arg) && 5 //acquireQueued:tryAcquire成功就setHead为当前节点,失败则阻塞当前线程 6 //addWaiter加入同步等待队列 7 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) 8 selfInterrupt(); 9 } 10 //加入等待队列 11 private 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; 17 if (pred != null) { 18 node.prev = pred; 19 if (compareAndSetTail(pred, node)) { 20 pred.next = node; 21 return node; 22 } 23 } 24 //首次插入,需要创建虚拟的head节点 25 enq(node); 26 return node; 27 } 28 private Node enq(final Node node) { 29 for (;;) { 30 Node t = tail; 31 // 如果 tail 是 null,就创建一个虚拟节点,同时指向 head 和 tail,称为 初始化。 32 if (t == null) { // Must initialize 33 if (compareAndSetHead(new Node())) 34 tail = head; 35 } else {// 如果不是 null 36 // 和 上个方法逻辑一样,将新节点追加到 tail 节点后面,并更新队列的 tail 为新节点。 37 // 只不过这里是死循环的,失败了还可以再来 。 38 node.prev = t; 39 if (compareAndSetTail(t, node)) { 40 t.next = node; 41 return t; 42 } 43 } 44 } 45 }