AQS基础

您所在的位置:网站首页 CLH是什么是谁 AQS基础

AQS基础

2024-07-17 15:13| 来源: 网络整理| 查看: 265

JDK1.8源码分析项目(中文注释)Github地址:

https://github.com/yuanmabiji/jdk1.8-sourcecode-blogs

1 什么是自旋锁和互斥锁?

由于CLH锁是一种自旋锁,那么我们先来看看自旋锁是什么?

自旋锁说白了也是一种互斥锁,只不过没有抢到锁的线程会一直自旋等待锁的释放,处于busy-waiting的状态,此时等待锁的线程不会进入休眠状态,而是一直忙等待浪费CPU周期。因此自旋锁适用于锁占用时间短的场合。

这里谈到了自旋锁,那么我们也顺便说下互斥锁。这里的互斥锁说的是传统意义的互斥锁,就是多个线程并发竞争锁的时候,没有抢到锁的线程会进入休眠状态即sleep-waiting,当锁被释放的时候,处于休眠状态的一个线程会再次获取到锁。缺点就是这一些列过程需要线程切换,需要执行很多CPU指令,同样需要时间。如果CPU执行线程切换的时间比锁占用的时间还长,那么可能还不如使用自旋锁。因此互斥锁适用于锁占用时间长的场合。

2 什么是CLH锁?

CLH锁其实就是一种是基于逻辑队列非线程饥饿的一种自旋公平锁,由于是 Craig、Landin 和 Hagersten三位大佬的发明,因此命名为CLH锁。

CLH锁原理如下:

首先有一个尾节点指针,通过这个尾结点指针来构建等待线程的逻辑队列,因此能确保线程线程先到先服务的公平性,因此尾指针可以说是构建逻辑队列的桥梁;此外这个尾节点指针是原子引用类型,避免了多线程并发操作的线程安全性问题;通过等待锁的每个线程在自己的某个变量上自旋等待,这个变量将由前一个线程写入。由于某个线程获取锁操作时总是通过尾节点指针获取到前一线程写入的变量,而尾节点指针又是原子引用类型,因此确保了这个变量获取出来总是线程安全的。

这么说肯定很抽象,有些小伙伴可能不理解,没关系,我们心中可以有个概念即可,后面我们会一步一图来彻彻底底把CLH锁弄明白。

3 为什么要学习CLH锁?

好了,前面我们对CLH锁有了一个概念后,那么我们为什么要学习CLH锁呢?

研究过AQS源码的小伙伴们应该知道,AQS是JUC的核心,而CLH锁又是AQS的基础,说核心也不为过,因为AQS就是用了变种的CLH锁。如果要学好Java并发编程,那么必定要学好JUC;学好JUC,必定要先学好AQS;学好AQS,那么必定先学好CLH。因此,这就是我们为什么要学习CLH锁的原因。

4 CLH锁详解

那么,下面我们先来看CLH锁实现代码,然后通过一步一图来详解CLH锁。

代码语言:javascript复制// CLHLock.java public class CLHLock { /** * CLH锁节点 */ private static class CLHNode { // 锁状态:默认为false,表示线程没有获取到锁;true表示线程获取到锁或正在等待 // 为了保证locked状态是线程间可见的,因此用volatile关键字修饰 volatile boolean locked = false; } // 尾结点,总是指向最后一个CLHNode节点 // 【注意】这里用了java的原子系列之AtomicReference,能保证原子更新 private final AtomicReference tailNode; // 当前节点的前继节点 private final ThreadLocal predNode; // 当前节点 private final ThreadLocal curNode; // CLHLock构造函数,用于新建CLH锁节点时做一些初始化逻辑 public CLHLock() { // 初始化时尾结点指向一个空的CLH节点 tailNode = new AtomicReference(new CLHNode()); // 初始化当前的CLH节点 curNode = new ThreadLocal() { @Override protected CLHNode initialValue() { return new CLHNode(); } }; // 初始化前继节点,注意此时前继节点没有存储CLHNode对象,存储的是null predNode = new ThreadLocal(); } /** * 获取锁 */ public void lock() { // 取出当前线程ThreadLocal存储的当前节点,初始化值总是一个新建的CLHNode,locked状态为false。 CLHNode currNode = curNode.get(); // 此时把lock状态置为true,表示一个有效状态, // 即获取到了锁或正在等待锁的状态 currNode.locked = true; // 当一个线程到来时,总是将尾结点取出来赋值给当前线程的前继节点; // 然后再把当前线程的当前节点赋值给尾节点 // 【注意】在多线程并发情况下,这里通过AtomicReference类能防止并发问题 // 【注意】哪个线程先执行到这里就会先执行predNode.set(preNode);语句,因此构建了一条逻辑线程等待链 // 这条链避免了线程饥饿现象发生 CLHNode preNode = tailNode.getAndSet(currNode); // 将刚获取的尾结点(前一线程的当前节点)付给当前线程的前继节点ThreadLocal // 【思考】这句代码也可以去掉吗,如果去掉有影响吗? predNode.set(preNode); // 【1】若前继节点的locked状态为false,则表示获取到了锁,不用自旋等待; // 【2】若前继节点的locked状态为true,则表示前一线程获取到了锁或者正在等待,自旋等待 while (preNode.locked) { System.out.println("线程" + Thread.currentThread().getName() + "没能获取到锁,进行自旋等待。。。"); } // 能执行到这里,说明当前线程获取到了锁 System.out.println("线程" + Thread.currentThread().getName() + "获取到了锁!!!"); } /** * 释放锁 */ public void unLock() { // 获取当前线程的当前节点 CLHNode node = curNode.get(); // 进行解锁操作 // 这里将locked至为false,此时执行了lock方法正在自旋等待的后继节点将会获取到锁 // 【注意】而不是所有正在自旋等待的线程去并发竞争锁 node.locked = false; System.out.println("线程" + Thread.currentThread().getName() + "释放了锁!!!"); // 小伙伴们可以思考下,下面两句代码的作用是什么?? CLHNode newCurNode = new CLHNode(); curNode.set(newCurNode); // 【优化】能提高GC效率和节省内存空间,请思考:这是为什么? // curNode.set(predNode.get()); } } 4.1 CLH锁的初始化逻辑

通过上面代码,我们缕一缕CLH锁的初始化逻辑先:

定义了一个CLHNode节点,里面有一个locked属性,表示线程线程是否获得锁,默认为false。false表示线程没有获取到锁或已经释放锁;true表示线程获取到了锁或者正在自旋等待。

注意,为了保证locked属性线程间可见,该属性被volatile修饰。

CLHLock有三个重要的成员变量尾节点指针tailNode,当前线程的前继节点preNode和当前节点curNode。其中tailNode是AtomicReference类型,目的是为了保证尾节点的线程安全性;此外,preNode和curNode都是ThreadLocal类型即线程本地变量类型,用来保存每个线程的前继CLHNode和当前CLHNode节点。最重要的是我们新建一把CLHLock对象时,此时会执行构造函数里面的初始化逻辑。此时给尾指针tailNode和当前节点curNode初始化一个locked状态为false的CLHNode节点,此时前继节点preNode存储的是null。4.2 CLH锁的加锁过程

我们再来看看CLH锁的加锁过程,下面再贴一遍加锁lock方法的代码:

代码语言:javascript复制// CLHLock.java /** * 获取锁 */ public void lock() { // 取出当前线程ThreadLocal存储的当前节点,初始化值总是一个新建的CLHNode,locked状态为false。 CLHNode currNode = curNode.get(); // 此时把lock状态置为true,表示一个有效状态, // 即获取到了锁或正在等待锁的状态 currNode.locked = true; // 当一个线程到来时,总是将尾结点取出来赋值给当前线程的前继节点; // 然后再把当前线程的当前节点赋值给尾节点 // 【注意】在多线程并发情况下,这里通过AtomicReference类能防止并发问题 // 【注意】哪个线程先执行到这里就会先执行predNode.set(preNode);语句,因此构建了一条逻辑线程等待链 // 这条链避免了线程饥饿现象发生 CLHNode preNode = tailNode.getAndSet(currNode); // 将刚获取的尾结点(前一线程的当前节点)付给当前线程的前继节点ThreadLocal // 【思考】这句代码也可以去掉吗,如果去掉有影响吗? predNode.set(preNode); // 【1】若前继节点的locked状态为false,则表示获取到了锁,不用自旋等待; // 【2】若前继节点的locked状态为true,则表示前一线程获取到了锁或者正在等待,自旋等待 while (preNode.locked) { try { Thread.sleep(1000); } catch (Exception e) { } System.out.println("线程" + Thread.currentThread().getName() + "没能获取到锁,进行自旋等待。。。"); } // 能执行到这里,说明当前线程获取到了锁 System.out.println("线程" + Thread.currentThread().getName() + "获取到了锁!!!"); }

虽然代码的注释已经很详细,我们还是缕一缕线程加锁的过程:

首先获得当前线程的当前节点curNode,这里每次获取的CLHNode节点的locked状态都为false;然后将当前CLHNode节点的locked状态赋值为true,表示当前线程的一种有效状态,即获取到了锁或正在等待锁的状态;因为尾指针tailNode的总是指向了前一个线程的CLHNode节点,因此这里利用尾指针tailNode取出前一个线程的CLHNode节点,然后赋值给当前线程的前继节点predNode,并且将尾指针重新指向最后一个节点即当前线程的当前CLHNode节点,以便下一个线程到来时使用;根据前继节点(前一个线程)的locked状态判断,若locked为false,则说明前一个线程释放了锁,当前线程即可获得锁,不用自旋等待;若前继节点的locked状态为true,则表示前一线程获取到了锁或者正在等待,自旋等待。

为了更通俗易懂,我们用一个图来说明。

**假如有这么一个场景:**有四个并发线程同时启动执行lock操作,假如四个线程的实际执行顺序为:threadA



【本文地址】


今日新闻


推荐新闻


    CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3