面试经典必问:ReentrantLock 中CLH队列

article/2025/11/5 18:37:21

ReentrantLock 中的加锁操作都是通过Syn这个抽象类来完成,具体解析在之前得博客已经分析过了,请参考:ReentrantLock AQS操作解析

得不到锁的线程,如何排队?

JUC中锁的排队策略,是基于CLH队列的变种实现的。因此,我们先看看啥是CLH队列

CLH队列

如上图所示,获取不到锁的线程,会进入队尾,然后自旋,直到其前驱线程释放锁。可能会有同学问为啥有2种指针在node上面,这个之后会在解释
这样做的好处:假设有1000个线程等待获取锁,锁释放后,只会通知队列中的第一个线程去竞争锁,减少了并发冲突。(ZK的分布式锁,为了避免惊群效应,也使用了类似的方式:获取不到锁的线程只监听前一个节点)

为什么说JUC中的实现是基于CLH的“变种”,因为原始CLH队列,一般用于实现自旋锁。而JUC中的实现,获取不到锁的线程,一般会时而阻塞,时而唤醒。

UC中的CLH队列实现

我们来看看AbstractQueuedSynchronizer类中的acquire方法实现

    public final void acquire(int arg) {//尝试获取锁if (!tryAcquire(arg) &&//获取不到,则进入等待队列,返回是否中断acquireQueued(addWaiter(Node.EXCLUSIVE), arg))//如果返回中断,则调用当前线程的interrupt()方法selfInterrupt();}

入队

如果线程调用tryAcquire(其最终实现是调用上面分析过的nonfairTryAcquire方法)获取锁失败。则首先调用addWaiter(Node.EXCLUSIVE)方法,将自己加入CLH队列的尾部。

    private Node addWaiter(Node mode) {//线程对应的NodeNode node = new Node(Thread.currentThread(), mode);// Try the fast path of enq; backup to full enq on failureNode pred = tail;//尾节点不为空if (pred != null) {//当前node的前驱指向尾节点node.prev = pred;//将当前node设置为新的尾节点//如果cas操作失败,说明线程竞争if (compareAndSetTail(pred, node)) {pred.next = node;return node;}}//lockfree的方式插入队尾enq(node);return node;}private Node enq(final Node node) {//经典的lockfree算法:循环+CASfor (;;) {Node t = tail;//尾节点为空if (t == null) { // Must initialize//初始化头节点if (compareAndSetHead(new Node()))tail = head;} else {node.prev = t;if (compareAndSetTail(t, node)) {t.next = node;return t;}}}}

入队过程,入下图所示

1 T0持有锁时,其CLH队列的头尾指针为NULL (没有任何node来获取锁时,锁会给到默认得head-node持有,也就是T0)

2 线程T1,此时请求锁,由于锁被T0占有。因此加入队列尾部。具体过程如下所示:
(1) 初始化头节点
(2) 初始化T1节点,入队,尾指针指向T1

3 此时如果有一个T10线程先于T1入队,则T1执行compareAndSetTail(t, node)会失败,然后回到for循环开始处,重新入队。

由自旋到阻塞

入队后,调用acquireQueued方法,时而自旋,时而阻塞,直到获取锁(或被取消)。

  final boolean acquireQueued(final Node node, int arg) {boolean failed = true;try {boolean interrupted = false;for (;;) {final Node p = node.predecessor();//其前驱是头结点,并且再次调用tryAcquire成功获取锁if (p == head && tryAcquire(arg)) {//将自己设为头结点setHead(node);p.next = null; // help GCfailed = false;//成功获取锁,返回return interrupted;}//没有得到锁时://shouldParkAfterFailedAcquire方法:返回是否需要阻塞当前线程//parkAndCheckInterrupt方法:阻塞当前线程,当线程再次唤醒时,返回是否被中断if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())//修改中断标志位interrupted = true;}} finally {if (failed)//获取锁失败,则将此线程对应的node的waitStatus改为CANCELcancelAcquire(node);}}private void setHead(Node node) {head = node;node.thread = null;node.prev = null;}/*** 获取锁失败时,检查并更新node的waitStatus。* 如果线程需要阻塞,返回true。*/private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {int ws = pred.waitStatus;//前驱节点的waitStatus是SIGNAL。if (ws == Node.SIGNAL)/* * SIGNAL状态的节点,释放锁后,会唤醒其后继节点。* 因此,此线程可以安全的阻塞(前驱节点释放锁时,会唤醒此线程)。*/return true;//前驱节点对应的线程被取消if (ws > 0) {do {//跳过此前驱节点node.prev = pred = pred.prev;} while (pred.waitStatus > 0);pred.next = node;} else {/*此时,需要将前驱节点的状态设置为SIGNAL。* waitStatus must be 0 or PROPAGATE.  Indicate that we* need a signal, but don't park yet.  Caller will need to* retry to make sure it cannot acquire before parking.*/compareAndSetWaitStatus(pred, ws, Node.SIGNAL);}return false;}//当shouldParkAfterFailedAcquire方法返回true,则调用parkAndCheckInterrupt方法阻塞当前线程private final boolean parkAndCheckInterrupt() {LockSupport.park(this);return Thread.interrupted();}

自旋过程,入下图所示



然后线程T2,加入了请求锁的队列,尾指针后移。

终上所述,每个得不到锁的线程,都会讲自己封装成Node,加入队尾,或自旋或阻塞,直到获取锁

锁的释放

前文提到,T1,T2在阻塞之前,都回去修改其前驱节点的waitStatus=-1。这是为什么?
我们看下锁释放的代码,便一目了然

    public final boolean release(int arg) {//修改锁计数器,如果计数器为0,说明锁被释放if (tryRelease(arg)) {Node h = head;//head节点的waitStatus不等于0,说明head节点的后继节点对应的线程,正在阻塞,等待被唤醒if (h != null && h.waitStatus != 0)//唤醒后继节点unparkSuccessor(h);return true;}return false;}private void unparkSuccessor(Node node) {/** If status is negative (i.e., possibly needing signal) try* to clear in anticipation of signalling.  It is OK if this* fails or if status is changed by waiting thread.*/int ws = node.waitStatus;if (ws < 0)compareAndSetWaitStatus(node, ws, 0);//后继节点Node s = node.next;//如果s被取消,跳过被取消节点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;}if (s != null)//唤醒后继节点LockSupport.unpark(s.thread);}

如代码所示,waitStatus=-1的作用,主要是告诉释放锁的线程:后面还有排队等待获取锁的线程,请唤醒他!

释放锁的过程,如图所示:


 

 

 


http://chatgpt.dhexx.cn/article/uF6vUhg1.shtml

相关文章

java clh_【Java并发编程实战】----- AQS(四):CLH同步队列

在【Java并发编程实战】—–“J.U.C”&#xff1a;CLH队列锁提过&#xff0c;AQS里面的CLH队列是CLH同步锁的一种变形。其主要从两方面进行了改造&#xff1a;节点的结构与节点等待机制。在结构上引入了头结点和尾节点&#xff0c;他们分别指向队列的头和尾&#xff0c;尝试获取…

CLH锁 、MCS锁

一.引文 1.1 SMP(Symmetric Multi-Processor)&#xff1a; 对称多处理器结构&#xff0c;指服务器中多个CPU对称工作&#xff0c;每个CPU访问内存地址所需时间相同。其主要特征是共享&#xff0c;包含对CPU&#xff0c;内存&#xff0c;I/O等进行共享。SMP能够保证内存一致性…

CLH锁

1.1 SMP(Symmetric Multi-Processor) 对称多处理器结构&#xff0c;它是相对非对称多处理技术而言的、应用十分广泛的并行技术。在这种架构中&#xff0c;一台计算机由多个CPU组成&#xff0c;并共享内存和其他资源&#xff0c;所有的CPU都可以平等地访问内存、I/O和外部中断。…

java clh_CLH锁学习

CLH锁即Craig, Landin, and Hagersten (CLH) locks&#xff0c;CLH锁是一个自旋锁&#xff0c;能确保无饥饿性&#xff0c;提供先来先服务的公平性。 何谓自旋锁&#xff1f;它是为实现保护共享资源而提出一种锁机制。其实&#xff0c;自旋锁与互斥锁比较类似&#xff0c;它们都…

aqs clh java_并发编程——详解 AQS CLH 锁

从 acquire 方法开始 —— 获取 为什么 AQS 需要一个虚拟 head 节点 reelase 方法如何释放锁 总结 前言 AQS 是 JUC 中的核心&#xff0c;其中封装了资源的获取和释放&#xff0c;在我们之前的 并发编程之 AQS 源码剖析 文章中&#xff0c;我们已经从 ReentranLock 那里分析了锁…

java clh_CLH lock 原理及JAVA实现

CLH 锁的名字也与他们的发明人的名字相关&#xff1a;Craig&#xff0c;Landin and Hagersten。 CLH Lock摘要 CLH lock is Craig, Landin, and Hagersten (CLH) locks, CLH lock is a spin lock, can ensure no hunger, provide fairness first come first service. The CLH l…

CLH队列

NUMA与SMP SMP(Symmetric Multi-Processor)&#xff0c;即对称多处理器结构&#xff0c;指服务器中多个CPU对称工作&#xff0c;每个CPU访问内存地址所需时间相同。其主要特征是共享&#xff0c;包含对CPU&#xff0c;内存&#xff0c;I/O等进行共享。SMP的优点是能够保证内存一…

CLH锁 简介

转自CLH锁 简介 - gaob2001的个人空间 - OSCHINA - 中文开源技术交流社区 概述 在学习Java AQS框架的时候发现加锁的逻辑非常奇怪&#xff0c;后来得知加锁逻辑是CLH锁的一个变种&#xff0c;于是了解一下&#xff0c;对于理解AQS框架有好处。 简介 CLH锁是有由Craig, Land…

CLH同步队列

文章转载自&#xff1a;https://blog.csdn.net/chenssy/article/details/60781148 AQS简介中提到了AQS内部维护着一个FIFO队列&#xff0c;该队列就是CLH同步队列。 CLH同步队列是一个FIFO双向队列&#xff0c;AQS依赖它来完成同步状态的管理&#xff0c;当前线程如果获取同步…

AQS的前菜—详解CLH队列锁

https://blog.csdn.net/weixin_47184173/article/details/115340014 什么是CLH队列锁 CLH锁其实就是一种基于逻辑队列非线程饥饿的一种自旋公平锁。当多线程竞争一把锁时&#xff0c;获取不到锁的线程&#xff0c;会排队进入CLH队列的队尾&#xff0c;然后自旋等待&#xff0c…

CLH(Craig, Landin, and Hagersten locks)机制

CLH(Craig, Landin, and Hagersten locks): 是一个自旋锁&#xff0c;能确保无饥饿性&#xff0c;提供先来先服务的公平性。CLH锁也是一种基于链表的可扩展、高性能、公平的自旋锁&#xff0c;申请线程只在本地变量上自旋&#xff0c;它不断轮询前驱的状态&#xff0c;如果发现…

【Java】Java函数式编程以及流的操作

文章目录 大纲lambda表达式一般内部类局部内部类匿名内部类 基于函数式接口的lambda表达式JDK8中自带的函数式接口Predicate判断Consumer消费Supplier供应商Function方法其他接口 方法引用和构造器引用静态方法引用非静态方法引用构造器引用 lambda表达式中的异常处理Currying …

【Java进阶】java函数式编程的使用

目录 1.目前Java中自带的函数式编程接口 2.java中使用函数式编程的案例 3.自定义函数式接口 4.自定义函数式接口的实现 简单一句话理解函数式编程&#xff0c;传统的方法调用我们都是传递参数&#xff0c;而函数式编程&#xff0c;传递的则是方法实现的过程。 1.目前Java中自…

Java教程:一文详解函数式编程

看懂了这篇-你就懂了函数式接口 ​ 函数式编程是一种编程规范或一种编程思想&#xff0c;简单可以理解问将运算或实现过程看做是函数的计算。 Java8为了实现函数式编程&#xff0c;提出了3个重要的概念&#xff1a;Lambda表达式、方法引用、函数式接口。现在很多公司都在使用l…

java中的函数式编程(一)

当你安安稳稳的学着java&#xff0c;慢慢开始写代码。 兢兢业业&#xff0c;本着面向对象的编程方式。 知道有一种叫做“面向过程”的方式&#xff0c;但是你不在意。 写了一段时间后有人告你&#xff0c;函数式编程很爽&#xff01; 你也看到了他给的一个实例&#xff0c;看着…

Java函数式编程(基础):第一部分

1.函数式编程有三个部分&#xff1a; 第一个部分是&#xff1a;Lambda表达式 第二个部分是&#xff1a;方法引用 第三个部分是&#xff1a;函数式接口 刚接触Lambda表达式的我&#xff0c;觉得它很神奇&#xff0c;能够用简短的代码&#xff0c;代替传统的编程方式 举一个简…

java-函数式编程浅谈

了解函数式编程的实际应用场景以及优点。 文章目录 什么是函数式编程函数式编程的使用原理解析 什么是函数式编程 以数学中的函数作为切入点&#xff0c;只关注参数之间的运算满足某种规则&#xff0c;例如zxy。 那么如何体现在编程中呢&#xff0c;熟知的function定义可以作…

Java 8函数式编程

函数式接口 一个接口中&#xff0c;有且只有一个抽象方法&#xff0c;这个接口就叫做函数式接口。常常使用FunctionalInterface注解作为编译校验。满足函数式接口的要求&#xff0c;才能校验通过&#xff0c;否则会在校验阶段失败。 接口中有且只能有一个抽象方法&#xff0c;…

【函数式编程实战】(一)Java演变与函数式编程

前言 &#x1f4eb;作者简介&#xff1a;小明Java问道之路&#xff0c;专注于研究计算机底层/Java/Liunx 内核&#xff0c;就职于大型金融公司后端高级工程师&#xff0c;擅长交易领域的高安全/可用/并发/性能的架构设计&#x1f4eb; &#x1f3c6;CSDN专家博主/Java领域优质…

Java8 函数式编程

文章目录 Java 函数式编程1. Lambda 表达式1.1 标准格式1.2 使用前提1.2.1 一个参数1.2.2 多个参数1.2.3 有返回值 1.3 省略简化1.4 函数式接口1.4.1 Supplier1.4.2 Consumer1.4.3 Predicate1.4.4 Function 1.5 方法引用1.5.1 对象 :: 实例方法1.5.2 类 :: 静态方法1.5.3 类 ::…