CLH自旋锁
JUC中显式锁基于AQS抽象队列同步器,而AQS是CLH锁的一个变种。
在争夺锁激烈的情况下,为了减少CAS空自旋(CAS需要CPU进行内部通信保证缓存一致性造成流量过大引起总线风暴),Java轻量级锁会升级为重量级锁,那么JUC基于CAS实现的轻量级锁如何避免总线风暴呢?答案是:使用队列对抢锁线性排队,最大程度上减少CAS操作数量。
CLH锁其实就是一种是基于队列(具体为单向链表)排队的自旋锁,申请加锁的线程首先会通过CAS操作在单向链表的尾部增加一个节点,之后该线程只需要在其前驱节点上进行普通自旋 ,等待前驱节点释放锁即可。由于CLH锁只有在节点入队时进行一下CAS的操作,在节点在加入队列之后,抢锁线程不需要进行CAS自旋,只需普通自旋即可。因此,在争用激烈的场景下, CLH锁能大大减少的CAS操作的数量,以避免CPU的总线风暴。
抢锁线程在队列尾部加入一个节点,然后仅在前驱节点上做普通自旋,它不断轮询前一个节点状态,如果发现前一个节点释放锁,当前节点抢锁成功。
CLH 加锁过程
首先明确,CLH是指向前节点的单链表,每个节点Node包括至少三个参数:前向指针、locked 状态变量、线程引用。 当一个线程加入抢锁队列时,创建新Node,然后通过CAS加入到CLH队列的尾部,前向指针指向前一个节点,尾指针指向新加入的节点。然后,这个节点的线程会对前向的Node进行普通自旋,循环判断前驱节点的locked属性是否为false,如果为false就表示前驱节点释放了锁 ,当前线程抢锁成功。此线程抢到锁后locked一直为true,直到释放锁为false。
注意:以上普通自旋与CAS的区别是 while 循环中是否有 Thread.yield(); 让出CPU时间片,CAS是没有yield的。另外还有一个尾指针,tail属性使用AtomicReference类型是为了使得多个线程并发操作tail时不会发生线程安全问题。 locked 为 true 表示此线程自旋等待中或者正在执行临界区代码,下一个节点需要等待,直到释放了锁 locked 为false。
//CAS自旋:将当前节点插入到队列的尾部
while (!tail.compareAndSet(preNode, curNode)){
preNode = tail.get();
}
// 普通自旋,监听前驱节点的locked变量,直到其值为false
// 若前继节点的locked状态为true,则表示前一个线程还在抢占或者占有锁
while (curNode.getPrevNode().isLocked()){
//让出CPU时间片,提高性能
Thread.yield();
}
CLH 释放锁的过程
当一个线程执行完临界区代码后释放锁,首先将前向指针指向null,然后将locked设置为false。此时前面的节点没有引用,将会被GC。此时它后面的节点捕获了前面的locked为false立即抢占锁执行临界区代码。