Java并发编程实战 AQS(四):CLH同步队列

本文详细解析了AQS中的CLH队列锁机制,包括节点结构与等待机制的改进,介绍了节点的状态及其意义,并解释了入列、出列、取消及挂起等关键流程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

【Java并发编程实战】—–“J.U.C”:CLH队列锁提过,AQS里面的CLH队列是CLH同步锁的一种变形。其主要从两方面进行了改造:节点的结构与节点等待机制。在结构上引入了头结点和尾节点,他们分别指向队列的头和尾,尝试获取锁、入队列、释放锁等实现都与头尾节点相关,并且每个节点都引入前驱节点和后后续节点的引用;在等待机制上由原来的自旋改成阻塞唤醒。其结构如下:

2015121100001

知道其结构了,我们再看看他的实现。在线程获取锁时会调用AQS的acquire()方法,该方法第一次尝试获取锁如果失败,会将该线程加入到CLH队列中:

[java]  view plain  copy
  1. public final void acquire(int arg) {  
  2.         if (!tryAcquire(arg) &&  
  3.             acquireQueued(addWaiter(Node.EXCLUSIVE), arg))  
  4.             selfInterrupt();  
  5.     }  

addWaiter:

[java]  view plain  copy
  1. private Node addWaiter(Node mode) {  
  2.         Node node = new Node(Thread.currentThread(), mode);  
  3.         Node pred = tail;  
  4.         if (pred != null) {  
  5.             node.prev = pred;  
  6.             if (compareAndSetTail(pred, node)) {  
  7.                 pred.next = node;  
  8.                 return node;  
  9.             }  
  10.         }  
  11.         enq(node);  
  12.         return node;  
  13.     }  

这是addWaiter()的实现,在厘清这段代码之前我们要先看一个更重要的东东,Node,CLH队列的节点。其源码如下:

[java]  view plain  copy
  1. static final class Node {  
  2.             /** 线程已被取消 */  
  3.             static final int CANCELLED =  1;  
  4.               
  5.             /** 当前线程的后继线程需要被unpark(唤醒) */  
  6.             static final int SIGNAL    = -1;  
  7.               
  8.             /** 线程(处在Condition休眠状态)在等待Condition唤醒 */  
  9.             static final int CONDITION = -2;  
  10.               
  11.             /** 共享锁 */  
  12.             static final Node SHARED = new Node();  
  13.             /** 独占锁  */  
  14.             static final Node EXCLUSIVE = null;  
  15.   
  16.             volatile int waitStatus;  
  17.   
  18.             /** 前继节点 */  
  19.             volatile Node prev;  
  20.   
  21.             /** 后继节点 */  
  22.             volatile Node next;  
  23.               
  24.             volatile Thread thread;  
  25.   
  26.             Node nextWaiter;  
  27.   
  28.             final boolean isShared() {  
  29.                 return nextWaiter == SHARED;  
  30.             }  
  31.   
  32.            /** 获取前继节点 */  
  33.             final Node predecessor() throws NullPointerException {  
  34.                 Node p = prev;  
  35.                 if (p == null)  
  36.                     throw new NullPointerException();  
  37.                 else  
  38.                     return p;  
  39.             }  
  40.   
  41.             /** 
  42.              * 三个构造函数 
  43.              */  
  44.             Node() {   
  45.             }  
  46.   
  47.             Node(Thread thread, Node mode) {     
  48.                 this.nextWaiter = mode;  
  49.                 this.thread = thread;  
  50.             }  
  51.   
  52.             Node(Thread thread, int waitStatus) {   
  53.                 this.waitStatus = waitStatus;  
  54.                 this.thread = thread;  
  55.             }  
  56.         }  

在这个源代码中有三个值(CANCELLED、SIGNAL、CONDITION)要特别注意,前面提到过CLH队列的节点都有一个状态位,该状态位与线程状态密切相关:

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

SIGNAL    = -1:其后继节点已经被阻塞了,到时需要进行唤醒操作;

CONDITION = -2:表示这个结点在条件队列中,因为等待某个条件而被阻塞;

0:新建节点一般都为0。

入列

在线程尝试获取锁的时候,如果失败了需要将该线程加入到CLH队列,入列中的主要流程是:tail执行新建node,然后将node的后继节点指向旧tail值。注意在这个过程中有一个CAS操作,采用自旋方式直到成功为止。其代码如下:

[java]  view plain  copy
  1. for(;;){  
  2.             Node t = tail;  
  3.             node.prev = t;  
  4.             if (compareAndSetTail(t, node)) {  
  5.                 t.next = node;  
  6.                 return t;  
  7.             }  
  8.         }  

其实这段代码在enq()方法中存在。

出列

当线程是否锁时,需要进行“出列”,出列的主要工作则是唤醒其后继节点(一般来说就是head节点),让所有线程有序地进行下去:

[java]  view plain  copy
  1. Node h = head;  
  2.             if (h != null && h.waitStatus != 0)  
  3.                 unparkSuccessor(h);  
  4.             return true;  

取消

线程因为超时或者中断涉及到取消的操作,如果某个节点被取消了,那个该节点就不会参与到锁竞争当中,它会等待GC回收。取消的主要过程是将取消状态的节点移除掉,移除的过程还是比较简单的。先将其状态设置为CANCELLED,然后将其前驱节点的pred执行其后继节点,当然这个过程仍然会是一个CAS操作:

[java]  view plain  copy
  1. node.waitStatus = Node.CANCELLED;  
  2. Node pred = node.prev;  
  3. Node predNext = pred.next;  
  4. Node next = node.next;  

挂起

我们了解了AQS的CLH队列相比原始的CLH队列锁,它采用了一种变形操作,将自旋机制改为阻塞机制。当前线程将首先检测是否为头结点且尝试获取锁,如果当前节点为头结点并成功获取锁则直接返回,当前线程不进入阻塞,否则将当前线程阻塞:

[java]  view plain  copy
  1. for (;;) {  
  2.     if (node.prev == head)  
  3. if(尝试获取锁成功){  
  4.          head=node;  
  5.          node.next=null;  
  6.          return;  
  7.      }  
  8.    阻塞线程  
  9. }  
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值