CLH lock 原理及JAVA实现
--喜欢记得关注我哟【shoshana】--
前记
JUC中的Lock中最核心的类AQS,其中AQS使用到了CLH队列的变种,故来研究一下CLH队列的原理及JAVA实现
一. CLH背景知识
CLH 锁的名字也与他们的发明人的名字相关:Craig,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 lock is a scalable, high performance, fairness and spin lock based on the list, the application thread spin only on a local variable, it constantly polling the precursor state, if it is found that the pre release lock end spin.
CLH锁是自旋锁的一种,对它的研究是因为AQS源代码中使用了CLH锁的一个变种,为了更好的理解AQS中使用锁的思想,所以决定先好好理解CLH锁
二. CLH原理
CLH也是一种基于单向链表(隐式创建)的高性能、公平的自旋锁,申请加锁的线程只需要在其前驱节点的本地变量上自旋,从而极大地减少了不必要的处理器缓存同步的次数,降低了总线和内存的开销。
三. Java代码实现
类图
public interface Lock {
void lock(); void unlock();
} public class QNode {
volatile boolean locked;
} import java.util.concurrent.atomic.AtomicReference; public class CLHLock implements Lock {
// 尾巴,是所有线程共有的一个。所有线程进来后,把自己设置为tail
private final AtomicReference<QNode> tail;
// 前驱节点,每个线程独有一个。
private final ThreadLocal<QNode> myPred;
// 当前节点,表示自己,每个线程独有一个。
private final ThreadLocal<QNode> myNode; public CLHLock() {
this.tail = new AtomicReference<QNode>(new QNode());
this.myNode = new ThreadLocal<QNode>() {
protected QNode initialValue() {
return new QNode();
}
};
this.myPred = new ThreadLocal<QNode>();
} @Override
public void lock() {
// 获取当前线程的代表节点
QNode node = myNode.get();
// 将自己的状态设置为true表示获取锁。
node.locked = true;
// 将自己放在队列的尾巴,并且返回以前的值。第一次进将获取构造函数中的那个new QNode
QNode pred = tail.getAndSet(node);
// 把旧的节点放入前驱节点。
myPred.set(pred);
// 判断前驱节点的状态,然后走掉。
while (pred.locked) {
}
} @Override
public void unlock() {
// unlock. 获取自己的node。把自己的locked设置为false。
QNode node = myNode.get();
node.locked = false;
myNode.set(myPred.get());
}
}
简单的看一下CLH的算法定义
the list, the application thread spin only on a local variable, it constantly polling the precursor state, if it is found that the pre release lock end spin.
基于list,线程仅在一个局部变量上自旋,它不断轮询前一个节点状态,如果发现前一个节点释放锁结束.
所以在java中使用了ThreadLocal作为具体实现,AtomicReference为了消除多个线程并发对tail引用Node的影响,核心方法lock()中分为3个步骤去实现
初始状态 tail指向一个node(head)节点
private final AtomicReference<Node> tail = new AtomicReference<Node>(new Node());
thread加入等待队列: tail指向新的Node,同时Prev指向tail之前指向的节点,在java代码中使用了getAndSet即CAS操作使用
Node pred = this.tail.getAndSet(node);
this.prev.set(pred);寻找当前线程对应的node的前驱node然后开始自旋前驱node的status判断是否可以获取lock
while (pred.locked);
同理unlock()方法,获取当前线程的node,设置lock status,将当前node指向前驱node(这样操作tail指向的就是前驱node等同于出队操作).至此CLH Lock的过程就结束了
测试CLHLock
public class CLHLockDemo2 { public static void main(String[] args) {
final Kfc kfc = new Kfc();
for (int i = 0; i < 10; i++) {
new Thread("eat" + i) {
public void run() {
kfc.eat();
}
}.start();
} }
} class Kfc {
private final Lock lock = new CLHLock();
private int i = 0; public void eat() {
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + ": " + --i);
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
} public void cook() {
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + ": " + ++i);
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
运行结果
eat1: -1
eat0: -2
eat3: -3
eat4: -4
eat7: -5
eat2: -6
eat5: -7
eat6: -8
eat8: -9
eat9: -10
四. CLH优缺点
CLH队列锁的优点是空间复杂度低(如果有n个线程,L个锁,每个线程每次只获取一个锁,那么需要的存储空间是O(L+n),n个线程有n个myNode,L个锁有L个tail),CLH的一种变体被应用在了JAVA并发框架中。唯一的缺点是在NUMA系统结构下性能很差,在这种系统结构下,每个线程有自己的内存,如果前趋结点的内存位置比较远,自旋判断前趋结点的locked域,性能将大打折扣,但是在SMP系统结构下该法还是非常有效的。一种解决NUMA系统结构的思路是MCS队列锁。
五. 了解与CLH对应的MCS自旋锁
MCS 自旋锁
MCS 的名称来自其发明人的名字:John Mellor-Crummey和Michael Scott。
MCS 的实现是基于链表的,每个申请锁的线程都是链表上的一个节点,这些线程会一直轮询自己的本地变量,来知道它自己是否获得了锁。已经获得了锁的线程在释放锁的时候,负责通知其它线程,这样 CPU 之间缓存的同步操作就减少了很多,仅在线程通知另外一个线程的时候发生,降低了系统总线和内存的开销。实现如下所示:
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
public class MCSLock {
public static class MCSNode {
volatile MCSNode next;
volatile boolean isWaiting = true; // 默认是在等待锁
}
volatile MCSNode queue;// 指向最后一个申请锁的MCSNode
private static final AtomicReferenceFieldUpdater<MCSLock, MCSNode> UPDATER = AtomicReferenceFieldUpdater
.newUpdater(MCSLock.class, MCSNode.class, "queue");
public void lock(MCSNode currentThread) {
MCSNode predecessor = UPDATER.getAndSet(this, currentThread);// step 1
if (predecessor != null) {
predecessor.next = currentThread;// step 2
while (currentThread.isWaiting) {// step 3
}
} else { // 只有一个线程在使用锁,没有前驱来通知它,所以得自己标记自己已获得锁
currentThread.isWaiting = false;
}
}
public void unlock(MCSNode currentThread) {
if (currentThread.isWaiting) {// 锁拥有者进行释放锁才有意义
return;
}
if (currentThread.next == null) {// 检查是否有人排在自己后面
if (UPDATER.compareAndSet(this, currentThread, null)) {// step 4
// compareAndSet返回true表示确实没有人排在自己后面
return;
} else {
// 突然有人排在自己后面了,可能还不知道是谁,下面是等待后续者
// 这里之所以要忙等是因为:step 1执行完后,step 2可能还没执行完
while (currentThread.next == null) { // step 5
}
}
}
currentThread.next.isWaiting = false;
currentThread.next = null;// for GC
}
}
MCS 的能够保证较高的效率,降低不必要的性能消耗,并且它是公平的自旋锁。
CLH 锁与 MCS 锁最大的不同是,MCS 轮询的是当前队列节点的变量,而 CLH 轮询的是当前节点的前驱节点的变量,来判断前一个线程是否释放了锁。
小结
CLH Lock是一种比较简单的自旋锁算法之一,因为锁的CAS操作涉及到了硬件的锁定(锁总线或者是锁内存)所以性能和CPU架构也密不可分,该兴趣的同学可以继续深入研究包括MCS锁等。CLH Lock是独占式锁的一种,并且是不可重入的锁,这篇文章是对AQS锁源代码分析的预热篇
参考内容:
https://segmentfault.com/a/1190000007094429
https://blog.csdn.net/faicm/article/details/80501465
https://blog.csdn.net/aesop_wubo/article/details/7533186
https://www.jianshu.com/p/0f6d3530d46b
https://blog.csdn.net/jjavaboy/article/details/78603477
CLH lock 原理及JAVA实现的更多相关文章
- CLH lock queue的原理解释及Java实现
目录 背景 原理解释 Java代码实现 定义QNode 定义Lock接口 定义CLHLock 使用场景 运行代码 代码输出 代码解释 CLHLock的加锁.释放锁过程 第一个使用CLHLock的线程自 ...
- Java并发包源码学习之AQS框架(二)CLH lock queue和自旋锁
上一篇文章提到AQS是基于CLH lock queue,那么什么是CLH lock queue,说复杂很复杂说简单也简单, 所谓大道至简: CLH lock queue其实就是一个FIFO的队列,队列 ...
- Ticket Lock, CLH Lock, MCS Lock
如果不用OS提供的mutex,我们该如何实现互斥锁?(不考虑重入的情况) 1. naive lock 最简单的想法是,搞一个volatile类型的共享变量flag,值可以是flase(无锁)或者tru ...
- 跳跃表-原理及Java实现
跳跃表-原理及Java实现 引言: 上周现场面试阿里巴巴研发工程师终面,被问到如何让链表的元素查询接近线性时间.笔者苦思良久,缴械投降.面试官告知回去可以看一下跳跃表,遂出此文. 跳跃表的引入 我们知 ...
- 一致性Hash算法原理,java实现,及用途
学习记录: 一致性Hash算法原理及java实现:https://blog.csdn.net/suifeng629/article/details/81567777 一致性Hash算法介绍,原理,及使 ...
- 从使用到原理学习Java线程池
线程池的技术背景 在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源.在Java中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收. 所 ...
- 自旋锁原理及java自旋锁
一.自旋锁的概念 首先是一种锁,与互斥锁相似,基本作用是用于线程(进程)之间的同步.与普通锁不同的是,一个线程A在获得普通锁后,如果再有线程B试图获取锁,那么这个线程B将会挂起(阻塞):试想下,如果两 ...
- synchronized与static synchronized 的差别、synchronized在JVM底层的实现原理及Java多线程锁理解
本Blog分为例如以下部分: 第一部分:synchronized与static synchronized 的差别 第二部分:JVM底层又是怎样实现synchronized的 第三部分:Java多线程锁 ...
- 【转载】从使用到原理学习Java线程池
线程池的技术背景 在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源.在Java中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收. 所 ...
随机推荐
- 将Redis设置为后台启动
Linux 在执行redis-server /etc/redis.conf 时默认开启的是一个前台的进程,也就是说启动的redis 就做不了其他的操作了,只有关闭redis 才能做其他的操作.非常的 ...
- Centos下安装LoadRunner负载机
aaarticlea/png;base64,iVBORw0KGgoAAAANSUhEUgAAAd0AAADfCAIAAAA84J6GAAAgAElEQVR4nJzb93cbR6In+uK/8v6Fd8
- python3 内建函数filter
Python内建的filter()函数用于过滤序列. 和map()类似,filter()也接收一个函数和一个序列.和map()不同的是,filter()把传入的函数依次作用于每个元素,然后根据返回值是 ...
- pymysql操作(老版本的,新版有基础不同)
导入库 import pymysql 创建链接 conn=pymysql.connect(host='127.0.0.1',port='3306',user='root',passwd='123456 ...
- eclipse设置序列化忽略生成serialVersionUID警告
java中的类想要序列化必须实现java.io.Serializable接口,一般实现接口都会用警告信息,提示生成serialVersionUID,虽然我觉得这个应该生成才合理.这里还是记录一下取消生 ...
- php导出数据到多个csv并打包压缩
1.不压缩直接下载 // 测试php导出大量数据到csv public function actionExportData() { // 设置不超时 set_time_limit(0); // 设置最 ...
- nginx $remote_addr 详解
1. 参考:https://blog.51cto.com/cuidehua/1827244?source=drt
- 回顾Python装饰器
函数装饰器(function decorator)可以对函数进行“标注”,给函数提供更多的特性. 在理解装饰器之前需要理解闭包(closure).Python3.0 引入了保留关键字 nonlocal ...
- opencv2配置window
https://opencv.org/ opencv2 opencv3 opencv4 (现在到4版本) 二值化 图像拉伸 灰度 图像腐蚀 车牌识别 配置:https://blog.csdn.n ...
- Caused by: java.lang.ClassNotFoundException: Cannot find class:
Caused by: org.apache.ibatis.builder.BuilderException: Error resolving class. Cause: org.apache.ibat ...