0
点赞
收藏
分享

微信扫一扫

Java小白系列(十):CLH

一、前言

本篇内容是为之后的内容AQS打下基础,AQS又称 Abstract Queued Synchronizer ,即抽象队列同步器,Java中,基于 AQS 实现了很多著名的锁,如:重入锁(ReentrantLock)、读写锁(ReadWriteLock)、共享锁(CountDownLatch)若设置为1则等同于互斥锁,等。因此,为了深入分析AQS,我将重要的内容都拆解出来,从一个一个的知识点逐个击破。

二、CLH

CLH(Craig,Landin and Hagersten)是三个人,共同发明了一个可扩展、高性能、公平且基于自旋锁的链表;链表中的每个线程只在本地自旋前一个节点的状态,即该节点(线程)不断自旋获取前一个节点的状态;每个节点都有一个状态(要么自旋,要么释放锁)。

在AQS中,用到的数据结构是 CLH 的变体:

      +-------+  prev  +-------+       +-------+
 head |   A   | <----- |   B   | <---- |   C   |  tail
      +-------+        +-------+       +-------+

上图是AQS中CLH的变体结构,该结构是:

  1. 一个 FIFO(first-in-first-out)队列;
  2. 新的等待获取锁的线程先加入队尾(tail);
  3. 如果队列是空,则第一个新加入的节点立即获得锁;
  4. 新加入的线程本地自旋前一个节点的状态(如 C 不断自旋获取 B 的状态);
  5. (当A释放锁时,B成为第一个节点),头节点并不能保证能够获得锁,只是有优先权,如果获取失败,则重新变为等待状态;

这里的内容,我们在分析《小白三,synchronized》时,谈到过 ContentionList、EntryList,AQS的变体CLH和它们一样,没有差别。

三、公平与非公平

『针对第5点进行解释』
CLH是公平队列,既然是公平队列,那么当『A』释放锁时,『B』成为首节点,应该是可以获得锁的,但为什么还会失败,而重新变成等待状态呢?如果首节点还获取不到锁,那CLH不就不公平了么?

  1. 『B』节点与新来的线程竞争获取锁,新的线程竞争成功,『B』失败,因此『B』重置为自旋等待状态;
  2. 新的线程不竞争,直接加入队尾,那么,此时『B』节点就能获取到锁;

如果采用非公平模式,则新来的线程如果竞争成功,则直接成为 first 节点,插入在 head 与 first 节点之间,而原 first 节点排在新的 first 节点之后进入等待状态并自旋。

我们在后面分析那些继承 AQS 实现的锁时,有些锁就支持非公平方式,如 ReentrantLock。

四、CLH的结构

CLH看似是个链表,但实际上并不存在,CLH中的每个节点类型为 Node,Node 中有两个索引,分别是 prev 和 next 将所有的节点串起来;在 AQS 中也只有两个索引节点,分别是 head 和 tail ,分别指向队列的头和尾;并没有一个成员变量类似 LinkedList 来管理所有的节点。

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
        
     static final class Node {
        // 共享模式
        static final Node SHARED = new Node();
        // 独占(排它)模式
        static final Node EXCLUSIVE = null;

        // 线程已被取消,当首节点释放锁后,
        // 开始查找下一个 waitStatus < 0 的节点,
        // 如果遇到已取消的线程,则移除
        static final int CANCELLED =  1;
        // 当前线程的后继线程需要被unpark(唤醒)
        // 后继节点处于等待状态,当前节点(为-1)被取消或者中断时会通知后继节点,
        // 使后继节点的线程得以运行
        static final int SIGNAL    = -1;
        // 当前节点处于等待队列,节点线程等待在Condition上,
        // 当其他线程对condition执行signall方法时,
        // 等待队列转移到同步队列,加入到对同步状态的获取
        static final int CONDITION = -2;
        // 与共享模式相关,在共享模式中,该状态标识结点的线程处于可运行状态
        static final int PROPAGATE = -3;

        // 当前节点的状态,默认是 0 
        volatile int waitStatus;

        volatile Node prev;
        volatile Node next;

        // 等待获取锁而自旋的线程
        volatile Thread thread;
        
        // Node既可以作为同步队列节点使用,也可以作为Condition的等待队列节点使用(将会在后面讲Condition时讲到)。
        // 在作为同步队列节点时,nextWaiter可能有两个值:EXCLUSIVE、SHARED标识当前节点是独占模式还是共享模式;
        // 在作为等待队列节点使用时,nextWaiter保存后继节点
        Node nextWaiter;

        final boolean isShared() {
            return nextWaiter == SHARED;
        }

        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }

        Node() {    // Used to establish initial head or SHARED marker
        }

        Node(Thread thread, Node mode) {     // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
        }

        Node(Thread thread, int waitStatus) { // Used by Condition
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }
}

我们可以看到,AQS 的 Node 结构不复杂,还算比较简单,下一篇将来开始讲解 AQS。

举报

相关推荐

0 条评论