11、JUC之AQS中的CLH队列

CLH队列

AQS内部维护着一个FIFO的队列,即CLH队列。AQS的同步机制就是依靠CLH队列实现的。CLH队列是FIFO的双端双向队列,实现公平锁。线程通过AQS获取锁失败,就会将线程封装成一个Node节点,插入队列尾。当有线程释放锁时,后尝试把队头的next节点占用锁。

CLH队列结构

《11、JUC之AQS中的CLH队列》

Node

CLH队列由Node对象组成,Node是AQS中的内部类。

重要属性

   //用于标识共享锁
   static final Node SHARED = new Node();

   //用于标识独占锁
   static final Node EXCLUSIVE = null;

   /**
    * 因为超时或者中断,节点会被设置为取消状态,被取消的节点时不会参与到竞争中的,他会一直保持取消状态不会转变为其他状态;
    */
   static final int CANCELLED =  1;

   /**
    * 当前节点释放锁的时候,需要唤醒下一个节点
    */
   static final int SIGNAL    = -1;

   /**
    * 节点在等待队列中,节点线程等待Condition唤醒
    */
   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队列执行

  1. 线程调用acquire方法获取锁,如果获取失败则会进入CLH队列
  public final void acquire(int arg) {
       if (!tryAcquire(arg) &&
           acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
           selfInterrupt();
   }

2.addWaiter(Node.EXCLUSIVE)方法会将当前线程封装成Node节点,追加在队尾。

private Node addWaiter(Node mode) {
       Node node = new Node(Thread.currentThread(), mode);
       // 获取原队尾
       Node pred = tail;
       if (pred != null) {
           node.prev = pred;
           //用cas更新 ,pred是原来队尾,作为预期值,node作为新值
           if (compareAndSetTail(pred, node)) {
               pred.next = node;
               return node;
           }
       }
       //前面cas更新失败后,再enq方法中循环用cas更新直到成功
       enq(node);
       return node;
}

  1. acquireQueued方法中会使线程自旋阻塞,直到获取到锁。
final boolean acquireQueued(final Node node, int arg) {
       boolean failed = true;
       try {
           boolean interrupted = false;
           for (;;) {
               //1. 拿到当前节点的前置节点
               final Node p = node.predecessor();
               
               //2. 如果当前节点的前置节点是头节点的话,就再次尝试获取锁
               if (p == head && tryAcquire(arg)) {
                   //成功获取锁后,将节点设置为头节点
                   setHead(node);
                   p.next = null; // help GC
                   failed = false;
                   return interrupted;
               }
               /**
               更改当前节点前置节点的waitStatus,只有前置节点的waitStatus=Node.SIGNAL,当前节点才有可能被唤醒。如果前置节点的waitStatus>0(即取消),则跳过取更前面的节点。
               */
               if (shouldParkAfterFailedAcquire(p, node) &&
               //通过Unsafe.park来阻塞线程
                   parkAndCheckInterrupt())
                   interrupted = true;
           }
       } finally {
           if (failed)
               cancelAcquire(node);
       }
   }

  1. 线程释放锁,从前面可以知道,获取到锁的线程会设置为CLH队列的头部。这里如果tryRelease返回true,且head的waitStatus!=0。就会更新head的waitStatus为0并且 唤醒线程head.next节点的线程。
 public final boolean release(int arg) { 
       //判断是否可以释放锁。
       if (tryRelease(arg)) {
           Node h = head;
           if (h != null && h.waitStatus != 0)
               unparkSuccessor(h);
           return true;
       }
       return false;
   }

  1. 更新head的waitStatus为0并且唤醒线程head.next节点的线程
 private void unparkSuccessor(Node node) {
       
       int ws = node.waitStatus;
       //waitStatus不是取消状态,就设置成0
       if (ws < 0)
           compareAndSetWaitStatus(node, ws, 0);

       
       //获取下个waitStatus不为取消的Node
       Node s = node.next;
       if (s == null || s.waitStatus > 0) {
           s = null;
           for (Node t = tail; t != null && t != node; t = t.prev)
               if (t.waitStatus <= 0)
                   s = t;
       }
       //LockSupport.unpark是调用了Unsafe.unpark,唤醒线程。
       if (s != null)
           LockSupport.unpark(s.thread);
   }
    原文作者:JUC
    原文地址: https://blog.csdn.net/qq_26680031/article/details/82348053
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞