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中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收. 所 ...
随机推荐
- android studio学习----偏好设置
主要找到设置的界面:Files->Settings , android studio里面没有preferences 1.字体大小设置 进入后你也许发现字体大小或者样式不符合你的习惯,比如我是觉得 ...
- ECharts将折线变平滑和去掉点的属性(转载)曲线变圆滑
本文链接:https://blog.csdn.net/sinat_36422236/article/details/62430114 series : [ { name:'your name', sy ...
- windows10删除多出的oem分区
某次windows升级后,磁盘管理里新出现一个500多M的OEM分区 其实系统里本来就有一个OEM分区是第一个分区,大小499M,可能因为这个分区太小,系统就又新建一个 因为在windows10分区后 ...
- 【转载】UNICODE与ASCII的区别
原文地址:https://blog.csdn.net/lx697/article/details/5914417 最近的项目涉及到了国际化的问题,由于之前并没有接触到UNICODE编码,因此,在项目期 ...
- MySQL查询优化一例——也说说 Using intersect
生产上面有一条sql查询很慢,需要7到8秒左右,简化之后的sql如下所示: SELECT mingxi.* FROM ( SELECT btjc01.id, department.`name` AS ...
- 在K8S集群中使用busybox-dig镜像,来作DNS解析分析
以前,判断K8S里的DNS功能是否正常时,得想很多办法. 如果有了busybox-dig镜像,则作这事就简单多了. 如下命令,直接部署 kubectl run busybox -it --image= ...
- django 自定义分页,网址存储缓存,CBV
1. 通过切片控制分页 自定义分页: from django.shortcuts import render # Create your views here. from app01.models i ...
- 字符串format()方法的基本使用
<模板字符串>.format(<逗号分隔的参数>) 其中,模板字符串是一个由字符串和槽组成的字符串,用来控制字符串和变量的显示效果.槽用大括号({})表示,对应format() ...
- VIJOS-P1320 清点人数
JDOJ 1427: VIJOS-P1320 清点人数 题目传送门 Description 初始时,火车上没有学生:当同学们开始上火车时,年级主任从第一节车厢出发走到最后一节车厢,每节车厢随时都有可能 ...
- docker的简单操作和端口映射
一:简介 Docker镜像 在Docker中容器是基于镜像启动的 镜像是启动容器的核心 镜像采用分层设计,最顶层为读写层 使用快照COW技术,确保底层不丢失 通过ifconfig(ip a)来查看d ...