CLH锁 简介

article/2025/11/5 19:52:25

转自CLH锁 简介 - gaob2001的个人空间 - OSCHINA - 中文开源技术交流社区

概述

在学习Java AQS框架的时候发现加锁的逻辑非常奇怪,后来得知加锁逻辑是CLH锁的一个变种,于是了解一下,对于理解AQS框架有好处。

简介

CLH锁是有由Craig, Landin, and Hagersten这三个人发明的锁,取了三个人名字的首字母,所以叫 CLH Lock。

CLH锁主要有一个QNode类,QNode类内部维护了一个boolean类型的变量,每个线程拥有一个前驱节点(myPred)和当前自己的节点(myNode),还有一个tail节点用于存储最后一个获取锁的线程的状态。CLH的从逻辑上形成一个锁等待队列从而实现加锁,CLH锁只支持按顺序加锁和解锁,不支持重入,不支持中断。

Java实现

public class CLHLock {private final AtomicReference<QNode> tail;private final ThreadLocal<QNode> myPred;private final ThreadLocal<QNode> myNode;private static class QNode {volatile boolean locked = false;}public CLHLock() {tail = new AtomicReference<QNode>(new QNode());myNode = new ThreadLocal<QNode>() {@Overrideprotected QNode initialValue() {return new QNode();}};myPred = new ThreadLocal<QNode>() {@Overrideprotected QNode initialValue() {return null;}};}public void lock() {QNode node = myNode.get();node.locked = true;QNode pred = tail.getAndSet(node);myPred.set(pred);while (pred.locked) {}}public void unlock() {QNode qnode = myNode.get();qnode.locked = false;myNode.set(myPred.get());// myNode.set(new QNode());}
}

代码很简单,tail变量的类型是AtomicReference用于保证原子操作,myNode是ThreadLocal类型的线程本地变量,保存当前节点的状态,myPred是ThreadLocal类型的线程本地变量,保存等待节点的状态。

测试

先通过简单的测试看一下效果

public static void main(String[] args) {Runnable runnable = new Runnable() {private int a;@Overridepublic void run() {for (int i = 0; i < 10000; i++) {a++;}System.out.println(Thread.currentThread().getName() + " a = " + a);}};new Thread(runnable).start();new Thread(runnable).start();new Thread(runnable).start();new Thread(runnable).start();new Thread(runnable).start();new Thread(runnable).start();new Thread(runnable).start();
}

声明了一个runnable对象,在线程内执行从1累加到10000,最后打印一个结果。在多线程的环境下这个a++不是一个原子操作,所以最后的计算结果一定是不正确的。

Thread-0 a = 11758
Thread-1 a = 15091
Thread-2 a = 18309
Thread-3 a = 18831
Thread-4 a = 23398
Thread-5 a = 23686
Thread-6 a = 33686

运行一次之后是这样的结果,和预期一样。然后加上锁看一下

public static void main(String[] args) {CLHLock lock = new CLHLock();Runnable runnable = new Runnable() {private int a;@Overridepublic void run() {lock.lock();for (int i = 0; i < 10000; i++) {a++;}System.out.println(Thread.currentThread().getName() + " a = " + a);lock.unlock();}};new Thread(runnable).start();new Thread(runnable).start();new Thread(runnable).start();new Thread(runnable).start();new Thread(runnable).start();new Thread(runnable).start();new Thread(runnable).start();
}

创建了一个CLHLock对象,调用了 lock.lock() 和 lock.unlock()。把整个run方法里面的内容都锁住,也就是等一个线程运行完了这个累加,下一个线程才可以继续执行,否则只能等着。

Thread-0 a = 10000
Thread-1 a = 20000
Thread-2 a = 30000
Thread-3 a = 40000
Thread-4 a = 50000
Thread-5 a = 60000
Thread-6 a = 70000

现在多次运行之后都是这个结果,加锁有效果。

原理分析

我们仔细分析一下lock和unlock的代码

public void lock() {QNode node = myNode.get();node.locked = true;QNode pred = tail.getAndSet(node);myPred.set(pred);while (pred.locked) {}
}public void unlock() {QNode qnode = myNode.get();qnode.locked = false;myNode.set(myPred.get());
}

锁的代码很简单就这么几行

结合这个图从上往下看,场景是有2个线程(Thread1, Thread2)同时想获取锁执行任务,左边是Thread1的执行情况,右边是Thread2的执行情况。这里的myNode myPred都是threadlocal类型的,下面说的myNode myPred的状态都是指myNode myPred内的QNode的状态。

第一行是初始化之后的状态,各个QNode都是false。

第二行第三行开始执行lock的操作,先将myNode的状态改为true,再将myNode的引用赋值给tail(tail.getAndSet(node) 的意思是将tail设置为node并返回tail原来的值,这里tail存的是一个QNode对象),再把tail原来的值赋值给myPred,通过一个while循环判断myPred的状态是否为true,为true表示锁正在被占用需要等待,一旦myPred变为false表示锁被释放了,可以执行。那么结合2个线程的情况来看,thread1调用lock方法成功获取到锁,thread2同时也调用lock方法想要获取锁,执行到 tail.getAndSet(node)的时候将tail设置为thread2.myNode,然后获取tail的旧值设置到thread2.myPred,那这个时候tail的旧值是刚才thread1的myNode,也就是说thread2在执行 while(pred.locked){} 等待的时候其实等待的是thread1.myNode状态变为false。tail存储的只是最后一个获取锁的线程的QNode,myNode一直在myPred上等待,通过一个while循环来实现独占锁。

第四行开始执行unlock操作,thread1任务执行完了将myNode的状态设置为false,此时thread2.myPred因为持有的是thread1.myNode的引用,所以也变为false退出循环,thread2得以执行下面的任务。

第五行,将myNode的值设置为myPred的引用。

看上去第五行似乎没有什么必要,网上关于这个的说话比较多,说一下我的理解。如果没有这行代码,在上面这个图中thread2线程在等待thread1.myNode的状态,假设thread1任务执行的速度非常快,在thread2的while的一次判断之后下一次判断开始之前,thread1执行完任务调用unlock解锁,然后马上又申请锁调用lock,又将thread1.myNode的状态设置为true了,同时thread1将tail值设置为thread1.myPred(这个时候tail节点储存的是thread2.myNode的引用),如此一来2个线程就变成了一个相互等待的情况,即死锁。那么在unlock的时候执行了myNode.set(myPred.get());的话,现在的myNode和thread2的myPred已经不是一个对象了,所以thread2.myPred会因为第四行的qnode.locked=false;退出循环等待。个人拙见,这里myNode.set(myPred.get());替换成myNode.set(new QNode());效果是一样的。

        高山个人意见,出现死锁,是因为线程释放锁以后,迅速再获取锁,且释放锁加获取锁的动作,是在下一个线程的一个while循环之间完成。所以解决问题的根本方法,是避免多个线程关注同一块内存的情况。不论是myNode.set(myPred.get());还是myNode.set(new QNode()),都是让两个线程,关注不同的内存地址。用myNode.set(myPred.get()),可以复用对象,可能会比myNode.set(new QNode())效果稍微好一点。

CLHLock示意图

参考CLH lock queue的原理解释及Java实现 - Mr靖哥哥 - 博客园@ 背景 相信大部分人在看AQS的时候都能看到注释上有这么一段话: The wait queue is a variant of a "CLH" (Craig, Landin, ahttps://www.cnblogs.com/mrcharleshu/p/13338957.html

这里的死锁发生的情况有一定的特殊性,myNode myPred是ThreadLocall类型的,而在线程池的场景下为了线程复用Thread一旦被创建就不会销毁,所以ThreadLocal类型的变量使用完一定要手动清理(下次执行之前如果不手动清理,ThreadLocal类型的变量还是上一次执行的结果),上面的第五行代码其实也是ThreadLocal使用完清理变量的意思,如果不使用线程池的话即使没有第五行代码也不会死锁。

这段代码就会引起死锁(卡住不动),运行多次可重现

public class CLHLock {...public void unlock() {QNode qnode = myNode.get();qnode.locked = false;//myNode.set(myPred.get());}...
}public static void main(String[] args) {ExecutorService executorService = Executors.newFixedThreadPool(2);CLHLock lock = new CLHLock();Runnable runnable = new Runnable() {private int a;@Overridepublic void run() {lock.lock();for (int i = 0; i < 100; i++) {a++;}System.out.println(Thread.currentThread().getName() + " a = " + a);lock.unlock();}};executorService.execute(runnable);executorService.execute(runnable);executorService.execute(runnable);executorService.execute(runnable);executorService.execute(runnable);executorService.execute(runnable);executorService.shutdown();
}


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

相关文章

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 类 ::…

入门 Java 函数式编程,看完这篇就清晰了

Java 在最开始是不支持函数式编程的&#xff0c;想来也好理解&#xff0c;因为在 Java 中类 Class 才是第一等公民&#xff0c;这就导致在 Java 中实现编程不是件那么容易的事儿&#xff0c;不过虽然难&#xff0c;但是结果我们也已经知道了&#xff0c;在 Java 8 这个大版本里…

Java函数式编程详解

Java从1.8以后引入了函数式编程&#xff0c;这是很大的一个改进。函数式编程的优点在提高编码的效率&#xff0c;增强代码的可读性。本文历时两个多月一点点写出来&#xff0c;即作为心得&#xff0c;亦作为交流。 1.Java函数式编程的语法&#xff1a; 使用Consumer作为示例&…

Java 函数式编程 详细介绍

在兼顾面向对象特性的基础上&#xff0c;Java语言通过Lambda表达式与方法引用等&#xff0c;为开发者打开了函数式编程的大门。 下面我们做一个初探。 Lambda的延迟执行 有些场景的代码执行后&#xff0c;结果不一定会被使用&#xff0c;从而造成性能浪费。而Lambda表达式是延…

Java基础函数式编程

本篇博文目录: 前言1.什么是函数式接口2.函数式编程(1) 使用Lambda表达式(2) Lambda表达式的进一步简化(3) Java内置函数式接口 3.方法引用(1) 方法引用的简单使用(2) 方法引用的分类 4.Stream API(1) 什么是Stream(2) 流式操作的执行流程(3) Stream的创建(4) Stream中间操作(5…

Java8新特性【函数式编程API、新时间日期处理API、Optional容器类】总结

文章目录 1、Lambda表达式1.1什么是Lambda表达式1.2从匿名类到 Lambda 的转换1.3Lambda表达式语法 2、函数式接口2.1什么是函数式接口2.2自定义函数式接口2.3内置核心函数式接口2.4接口中常用的默认方法 3、方法引用与构造器引用3.1 推荐用法3.2 基本格式3.3 语法详解(了解)3.3…

一文带你入门 Java 函数式编程

Java 在最开始是不支持函数式编程的&#xff0c;想来也好理解&#xff0c;因为在 Java 中类 Class 才是第一等公民&#xff0c;这就导致在 Java 中实现编程不是件那么容易的事儿&#xff0c;不过虽然难&#xff0c;但是结果我们也已经知道了&#xff0c;在 Java 8 这个大版本里…

Oracle数据库 存储过程入门

oracle存储过程:简单入门 一、定义 存储过程是一组为了完成特定功能的SQL语句&#xff0c;经编译后存储在数据库中。点击查看优缺点。二、存储过程简单入门 ***第一个存储过程&#xff1a;打印hello word, my name is stored procedure内容*** create or replace procedure m…

数据库储存过程超简单实例

网上看了半天都没找到一个完整储存过程从创建到调用的实例,于是自己写了一个简单的实例. 数据库创建存储过程,定义个函数 格式如下,开头DELIMITER //和结尾/DELIMITER 和BEGIN 和 END 是固定格式 定了一个叫test2()的方法(在mapper.xml中会指定这个函数名),in表示入参,varc…