CLH同步队列

什么是同步队列(CLH)

同步队列

一个FIFO双向队列,队列中每个节点等待前驱节点释放共享状态(锁)被唤醒就可以了。

AQS如何使用它?

AQS依赖它来完成同步状态的管理,当前线程如果获取同步状态失败时,AQS则会将当前线程已经等待状态等信息构造成一个节点(Node)并将其加入到CLH同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点唤醒(公平锁),使其再次尝试获取同步状态

CLH同步队列是一个FIFO双向队列,AQS依赖它来完成同步状态的管理,当前线程如果获取同步状态失败时,AQS则会将当前线程已经等待状态等信息构造成一个节点(Node)并将其加入到CLH同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点唤醒(公平锁),使其再次尝试获取同步状态。
在CLH同步队列中,一个节点表示一个线程,它保存着线程的引用(thread)、状态(waitStatus)、前驱节点(prev)、后继节点(next)

CLH同步队列的结构图

在这里插入图片描述
这里是基于CAS(保证线程的安全)来设置尾节点的。

Node节点面貌?

static final class Node {
        // 节点分为两种模式: 共享式和独占式
        /** 共享式 */
        static final Node SHARED = new Node();
        /** 独占式 */
        static final Node EXCLUSIVE = null;

        /** 等待线程超时或者被中断、需要从同步队列中取消等待(也就是放弃资源的竞争),此状态不会在改变 */
        static final int CANCELLED =  1;
        /** 后继节点会处于等待状态,当前节点线程如果释放同步状态或者被取消则会通知后继节点线程,使后继节点线程的得以运行 */
        static final int SIGNAL    = -1;
        /** 节点在等待队列中,线程在等待在Condition 上,其他线程对Condition调用singnal()方法后,该节点加入到同步队列中。 */
        static final int CONDITION = -2;
        /**
         * 表示下一次共享式获取同步状态的时会被无条件的传播下去。
         */
        static final int PROPAGATE = -3;

        /**等待状态*/
        volatile int waitStatus;

        /**前驱节点 */
        volatile Node prev;

        /**后继节点*/
        volatile Node next;

        /**获取同步状态的线程 */
        volatile Thread thread;

        /**链接下一个等待状态 */
        Node nextWaiter;
        
        // 下面一些方法就不贴了
    }

入列操作

如上图了解了同步队列的结构, 我们在分析其入列操作在简单不过。无非就是将tail(使用CAS保证原子操作)指向新节点,新节点的prev指向队列中最后一节点(旧的tail节点),原队列中最后一节点的next节点指向新节点以此来建立联系,来张图帮助大家理解。

在这里插入图片描述

源码

private Node addWaiter(Node mode) {
// 以给定的模式来构建节点, mode有两种模式 
//  共享式SHARED, 独占式EXCLUSIVE;
  Node node = new Node(Thread.currentThread(), mode);
    // 尝试快速将该节点加入到队列的尾部
    Node pred = tail;
     if (pred != null) {
        node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        // 如果快速加入失败,则通过 anq方式入列
        enq(node);
        return node;
    }

先通过addWaiter(Node node)方法尝试快速将该节点设置尾成尾节点,设置失败走enq(final Node node)方法

private Node enq(final Node node) {
// CAS自旋,直到加入队尾成功        
for (;;) {
    Node t = tail;
        if (t == null) { // 如果队列为空,则必须先初始化CLH队列,新建一个空节点标识作为Hader节点,并将tail 指向它
            if (compareAndSetHead(new Node()))
                tail = head;
            } else {// 正常流程,加入队列尾部
                node.prev = t;
                    if (compareAndSetTail(t, node)) {
                        t.next = node;
                        return t;
                }
            }
        }
    }



通过“自旋”也就是死循环的方式来保证该节点能顺利的加入到队列尾部,只有加入成功才会退出循环,否则会一直循序直到成功。

上述两个方法都是通过compareAndSetHead(new Node())方法来设置尾节点,以保证节点的添加的原子性(保证节点的添加的线程安全。)

出列操作

步队列(CLH)遵循FIFO,首节点是获取同步状态的节点,首节点的线程释放同步状态后,将会唤醒它的后继节点(next),而后继节点将会在获取同步状态成功时将自己设置为首节点。在队列同步器中,头节点是成功获取到同步状态的节点,而头节点的线程释放了同步状态后,将会唤醒其他后续节点,后继节点的线程被唤醒后需要检查自己的前驱节点是否是头节点,如果是则尝试获取同步状态,这个过程非常简单。如下图
在这里插入图片描述
设置首节点是通过获取同步状态成功的线程来完成的(获取同步状态是通过CAS来完成),只能有一个线程能够获取到同步状态,因此设置头节点的操作并不需要CAS来保证,只需要将首节点设置为其原首节点的后继节点并断开原首节点的next(等待GC回收)应用即可。

CLH锁原理

首先有一个尾节点指针,通过这个尾结点指针来构建等待线程的逻辑队列,当有新的节点加入队列时,尾节点指针会指向这个新加入的节点,并将原本的尾节点变为当前新加入节点的前驱节点。因此能确保线程线程先到先服务的公平性,尾指针可以说是构建逻辑队列的桥梁;此外这个尾节点指针是原子引用类型,避免了多线程并发操作的线程安全性问题;
通过等待锁的每个线程在自己的某个变量上自旋等待,这个变量指向自己的前驱节点中的变量,通过不断地自旋,感知到前驱节点的变化后成功获取到锁。

CLH锁的优点

没有惊群效应。假设有1000个线程等待获取锁,锁释放后,只会通知队列中的第一个线程去竞争锁,避免了同时唤醒大量线程 在瞬间争抢CPU资源,避免了惊群效应。(此处仅仅是不会对锁过度的争抢,也就是公平锁的好处。但是自旋锁的实现方式依然消耗CPU)
CLH队列锁的长处是空间复杂度低(假设有n个线程。L个锁,每一个线程每次仅仅获取一个锁,那么须要的存储空间是O(L+n),n个线程有n个myNode。L个锁有L个tail)。

CLH锁的缺点

在NUMA系统结构下性能稍差。在这样的系统结构下,每一个线程有自己的内存,假设前趋结点的内存位置比較远。自旋推断前趋结点的locked域,性能将大打折扣,在SMP结构下还是非常有效的。【CLH自旋在前驱节点上,访问的是其他线程的变量值,在NUMA架构下,其他线程变量有可能是对端CPU的高速缓存,因此更适合SMP架构】

总结

完后我们来总一下,同步队列就是一个FIFO双向对队列,其每个节点包含获取同步状态失败的线程应用、等待状态、前驱节点、后继节点、节点的属性类型以及名称描述。

其入列操作也就是利用CAS(保证线程安全)来设置尾节点,出列就很简单了直接将head指向新头节点并断开老头节点联系就可以了。

在lock锁的加锁实现,线程池的实现均利用了同步队列

CLH队列其实属于我们学习AQS的前菜。但是只有深入研究后,才知道CLH存在什么问题(CLH每一个线程都是一个自旋锁,非常消耗CPU),以及AQS在CLH的基础上做了哪些优化。我们可以看到公平锁就是最初的实现理念就是CLH队列。

在队列同步器中,同步队列为什么是双向链表,而等待队列是单链表

在队列同步器中,头节点是成功获取到同步状态的节点,而头节点的线程释放了同步状态后,将会唤醒其他后续节点,后继节点的线程被唤醒后需要检查自己的前驱节点是否是头节点,如果是则尝试获取同步状态。

所以为了能让后继节点获取到其前驱节点,同步队列便设置为双向链表,而等待队列没有这样的需求,就为单链表。

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
CLH队列和条件队列是两个不同的概念。 CLH队列是AQS(AbstractQueuedSynchronizer)内部维护的一种FIFO队列,用于实现同步机制。它采用双向链表保存,使用prev和next相互链接,每个节点表示一个线程,按照线程的先后顺序排队等待获取锁资源。CLH队列的特点是线程之间通过不断自旋来检查前一个线程是否已经释放锁资源,从而实现高效的等待队列。 条件队列则是AQS中的一种数据结构,用于实现线程的等待与唤醒机制。条件队列是使用单向列表保存的,使用nextWaiter来连接节点。每个节点表示一个等待条件的线程,当线程调用await()方法时,它将被移动到条件队列中等待。当满足某个条件时,其他线程可以调用signal()或signalAll()方法来唤醒条件队列中的线程。 总结起来,CLH队列用于实现同步机制中的线程排队等待获取锁资源,而条件队列用于实现线程的等待与唤醒机制。它们分别采用不同的数据结构和链接方式。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [并发之AQS原理(二) CLH队列与Node解析](https://blog.csdn.net/weixin_30561177/article/details/99097474)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* [浅谈Java并发 J.U.C之AQS:CLH同步队列](https://download.csdn.net/download/weixin_38694336/12749163)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值