--喜欢记得关注我哟【shoshana】--

前记

JUC中的Lock中最核心的类AQS,其中AQS使用到了CLH队列的变种,故来研究一下CLH队列的原理及JAVA实现

一. CLH背景知识

SMP(Symmetric Multi-Processor)。即对称多处理器结构,指server中多个CPU对称工作,每一个CPU訪问内存地址所需时间同样。其主要特征是共享,包括对CPU,内存,I/O等进行共享。SMP的长处是可以保证内存一致性。缺点是这些共享的资源非常可能成为性能瓶颈。随着CPU数量的添加,每一个CPU都要訪问同样的内存资源,可能导致内存訪问冲突,可能会导致CPU资源的浪费。经常使用的PC机就属于这样的。
NUMA(Non-Uniform Memory Access)非一致存储訪问,将CPU分为CPU模块,每一个CPU模块由多个CPU组成,而且具有独立的本地内存、I/O槽口等,模块之间能够通过互联模块相互訪问,訪问本地内存的速度将远远高于訪问远地内存(系统内其他节点的内存)的速度,这也是非一致存储訪问NUMA的由来。NUMA长处是能够较好地解决原来SMP系统的扩展问题,缺点是因为訪问远地内存的延时远远超过本地内存,因此当CPU数量添加时。系统性能无法线性添加。

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个步骤去实现

  1. 初始状态 tail指向一个node(head)节点

    private final AtomicReference<Node> tail = new AtomicReference<Node>(new Node());
  2. thread加入等待队列: tail指向新的Node,同时Prev指向tail之前指向的节点,在java代码中使用了getAndSet即CAS操作使用

    Node pred = this.tail.getAndSet(node);
    this.prev.set(pred);
  3. 寻找当前线程对应的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 锁的原理大致相同,都是各个线程轮询各自关注的变量,来避免多个线程对同一个变量的轮询,从而从 CPU 缓存一致性的角度上减少了系统的消耗。
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实现的更多相关文章

  1. CLH lock queue的原理解释及Java实现

    目录 背景 原理解释 Java代码实现 定义QNode 定义Lock接口 定义CLHLock 使用场景 运行代码 代码输出 代码解释 CLHLock的加锁.释放锁过程 第一个使用CLHLock的线程自 ...

  2. Java并发包源码学习之AQS框架(二)CLH lock queue和自旋锁

    上一篇文章提到AQS是基于CLH lock queue,那么什么是CLH lock queue,说复杂很复杂说简单也简单, 所谓大道至简: CLH lock queue其实就是一个FIFO的队列,队列 ...

  3. Ticket Lock, CLH Lock, MCS Lock

    如果不用OS提供的mutex,我们该如何实现互斥锁?(不考虑重入的情况) 1. naive lock 最简单的想法是,搞一个volatile类型的共享变量flag,值可以是flase(无锁)或者tru ...

  4. 跳跃表-原理及Java实现

    跳跃表-原理及Java实现 引言: 上周现场面试阿里巴巴研发工程师终面,被问到如何让链表的元素查询接近线性时间.笔者苦思良久,缴械投降.面试官告知回去可以看一下跳跃表,遂出此文. 跳跃表的引入 我们知 ...

  5. 一致性Hash算法原理,java实现,及用途

    学习记录: 一致性Hash算法原理及java实现:https://blog.csdn.net/suifeng629/article/details/81567777 一致性Hash算法介绍,原理,及使 ...

  6. 从使用到原理学习Java线程池

    线程池的技术背景 在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源.在Java中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收. 所 ...

  7. 自旋锁原理及java自旋锁

    一.自旋锁的概念 首先是一种锁,与互斥锁相似,基本作用是用于线程(进程)之间的同步.与普通锁不同的是,一个线程A在获得普通锁后,如果再有线程B试图获取锁,那么这个线程B将会挂起(阻塞):试想下,如果两 ...

  8. synchronized与static synchronized 的差别、synchronized在JVM底层的实现原理及Java多线程锁理解

    本Blog分为例如以下部分: 第一部分:synchronized与static synchronized 的差别 第二部分:JVM底层又是怎样实现synchronized的 第三部分:Java多线程锁 ...

  9. 【转载】从使用到原理学习Java线程池

    线程池的技术背景 在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源.在Java中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收. 所 ...

随机推荐

  1. 将Redis设置为后台启动

    Linux 在执行redis-server  /etc/redis.conf 时默认开启的是一个前台的进程,也就是说启动的redis 就做不了其他的操作了,只有关闭redis 才能做其他的操作.非常的 ...

  2. Centos下安装LoadRunner负载机

    aaarticlea/png;base64,iVBORw0KGgoAAAANSUhEUgAAAd0AAADfCAIAAAA84J6GAAAgAElEQVR4nJzb93cbR6In+uK/8v6Fd8

  3. python3 内建函数filter

    Python内建的filter()函数用于过滤序列. 和map()类似,filter()也接收一个函数和一个序列.和map()不同的是,filter()把传入的函数依次作用于每个元素,然后根据返回值是 ...

  4. pymysql操作(老版本的,新版有基础不同)

    导入库 import pymysql 创建链接 conn=pymysql.connect(host='127.0.0.1',port='3306',user='root',passwd='123456 ...

  5. eclipse设置序列化忽略生成serialVersionUID警告

    java中的类想要序列化必须实现java.io.Serializable接口,一般实现接口都会用警告信息,提示生成serialVersionUID,虽然我觉得这个应该生成才合理.这里还是记录一下取消生 ...

  6. php导出数据到多个csv并打包压缩

    1.不压缩直接下载 // 测试php导出大量数据到csv public function actionExportData() { // 设置不超时 set_time_limit(0); // 设置最 ...

  7. nginx $remote_addr 详解

    1. 参考:https://blog.51cto.com/cuidehua/1827244?source=drt

  8. 回顾Python装饰器

    函数装饰器(function decorator)可以对函数进行“标注”,给函数提供更多的特性. 在理解装饰器之前需要理解闭包(closure).Python3.0 引入了保留关键字 nonlocal ...

  9. opencv2配置window

    https://opencv.org/ opencv2 opencv3  opencv4 (现在到4版本) 二值化  图像拉伸 灰度  图像腐蚀 车牌识别 配置:https://blog.csdn.n ...

  10. Caused by: java.lang.ClassNotFoundException: Cannot find class:

    Caused by: org.apache.ibatis.builder.BuilderException: Error resolving class. Cause: org.apache.ibat ...