CAS、AQS、ReentrantLock机制以原理

1、CAS

1.1 基本概念

CAS 是 compare and swap 的简写,即比较并交换。它是指一种操作机制,而不是某个具体的类或方法。在 Java 平台上对这种操作进行了包装。在 Unsafe 类中,调用代码如下

 这里无法用Unsafe类看,我使用的是AtomicInteger查看的源码

 这里是三个分别是内存位置 V,旧的预期值 A 和新的值 B。操作时,先从内存位置读取到值,然后和预期值A比较。如果相等,则将此内存位置的值改为新值 B,返回 true。如果不相等,说明和其他线程冲突了,则不做任何改变,返回 false。

这种机制在不阻塞其他线程的情况下避免了并发冲突,比独占锁的性能高很多。 CAS 在 Java 的原子类和并发包中有大量使用。

1.2底层实现

 CAS 主要分三步,读取-比较-修改。其中比较是在检测是否有冲突,如果检测到没有冲突后,其他线程还能修改这个值,那么 CAS 还是无法保证正确性。所以最关键的是要保证比较-修改这两步操作的原子性。

CAS 底层是靠调用 CPU 指令集的 cmpxchg 完成的,它是 x86 和 Intel 架构中的 compare and exchange 指令。在多核的情况下,这个指令也不能保证原子性,需要在前面加上 lock 指令。lock 指令可以保证一个 CPU 核心在操作期间独占一片内存区域。那么 这又是如何实现的呢?

在处理器中,一般有两种方式来实现上述效果:

总线锁和缓存锁。

在多核处理器的结构中,CPU 核心并不能直接访问内存,而是统一通过一条总线访问。总线锁就是锁住这条总线,使其他核心无法访问内存。这种方式代价太大了,会导致其他核心停止工作。

而缓存锁并不锁定总线,只是锁定某部分内存区域。当一个 CPU 核心将内存区域的数据读取到自己的缓存区后,它会锁定缓存对应的内存区域。锁住期间,其他核心无法操作这块内存区域。

CAS 就是通过这种方式实现比较和交换操作的原子性的。值得注意的是, CAS 只是保证了操作的原子性,并不保证变量的可见性,因此变量需要加上 volatile 关键字。

1.3 ABA问题

上面提到,CAS 保证了比较和交换的原子性。但是从读取到开始比较这段期间,其他核心仍然是可以修改这个值的。如果核心将 A 修改为 B,CAS 可以判断出来。但是如果核心将 A 修改为 B 再修改回 A。那么 CAS 会认为这个值并没有被改变,从而继续操作。这是和实际情况不符的。解决方案是加一个版本号。

2.AQS

2.1基本概念 

 全称 AbstractQueuedSynchronizer。AQS 中有两个重要的成员:

  • 成员变量 state。用于表示锁现在的状态,用 volatile 修饰,保证内存一致性。同时所用对 state 的操作都是使用 CAS 进行的。state 为0表示没有任何线程持有这个锁,线程持有该锁后将 state 加1,释放时减1。多次持有释放则多次加减。
  • 还有一个双向链表,链表除了头结点外,每一个节点都记录了线程的信息,代表一个等待线程。这是一个 FIFO 的链表

3. ReentrantLock

 3.1 基本概念

Lock和 java.io.Serializable接口,并提供了与synchronized相同的互斥性和内存可见性

public class ReentrantLock implements Lock, java.io.Serializable {
.....
}

他是非公平(默认)也可以是非公平的,是支持重入的锁

3.2 底层实现 

 ReentrantLock与AQS的关系非常密切

  abstract static class Sync extends AbstractQueuedSynchronizer {
。。。。
}

 ReentrantLock的使用则是这样子

Lock lock = new ReentrantLock();lock.lock();try{//更新对象状态//捕获异常,并在必须时恢复不变性条件}catch (Exception e){e.printStackTrace();} finally {lock.unlock();}

 因为他继承了Lock的接口i,我们也要讲下Lock的方法

方法名称方法描述
lock用来获取锁,如果锁已被其他线程获取,则进行等待
tryLock表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待
tryLock(long time, TimeUnit unit)和tryLock()类似,区别在于它在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true
lockInterruptibly获取锁,如果获取锁失败则进行等到,如果等待的线程被中断会相应中断信息
unlock释放锁的操作
newCondition获取Condition对象,该组件和当前的锁绑定,当前线程只有获得了锁,才能调用该组件wait()方法,而调用后,当前线程释放锁

 ReentrantLock 也实现了上面接口的内容,同时 ReentrantLock 提供了 公平锁和 非公平锁两种模式,如果没有特别的去指定使用何种方式,那么 ReentrantLock 会默认为 非公平锁,首先我们来看一下 ReentrantLock 的构造函数:

 /*** 无参的构造函数*/public ReentrantLock() {sync = new NonfairSync();}/*** 有参构造函数* 参数为布尔类型*/public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();}

我们会发现,在默认无参构造的是非公平锁,即对应了我上文所说的默认是非公平的锁。

我们这里就要说下非公平锁是怎么个东西了。

3.2.1 非公平锁NonfairSync.lock()

 3.2.1.1 lock方法详解

当我们调用 ReentrantLock 的 lock() 方法的时候,实际上是调用了 NonfairSync 的 lock() 方法,代码如下:

static final class NonfairSync extends Sync {private static final long serialVersionUID = 7316153563782823691L;/*** Performs lock.  Try immediate barge, backing up to normal* acquire on failure.*/final void lock() {//这个方法先用CAS操作,去尝试抢占该锁// 快速尝试将state从0设置成1,如果state=0代表当前没有任何一个线程获得了锁if (compareAndSetState(0, 1))//state设置成1代表获得锁成功//如果成功,就把当前线程设置在这个锁上,表示抢占成功,在重入锁的时候需要setExclusiveOwnerThread(Thread.currentThread());else//如果失败,则调用 AbstractQueuedSynchronizer.acquire() 模板方法,等待抢占。acquire(1);}protected final boolean tryAcquire(int acquires) {return nonfairTryAcquire(acquires);}}

 调用 acquire(1) 实际上使用的是 AbstractQueuedSynchronizer 的 acquire() 方法,它是一套锁抢占的模板,acquire() 代码比较简单:

public final void acquire(int arg) {//先去尝试获取锁,如果没有获取成功,就在CLH队列中增加一个当前线程的节点,表示等待抢占。//然后进入CLH队列的抢占模式,进入的时候也会去执行一次获取锁的操作,如果还是获取不到,//就调用LockSupport.park() 将当前线程挂起。那么当前线程什么时候会被唤醒呢?当//持有锁的那个线程调用 unlock() 的时候,会将CLH队列的头节点的下一个节点上的线程//唤醒,调用的是 LockSupport.unpark() 方法。if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();}

acquire() 会先调用 tryAcquire() 这个钩子方法去尝试获取锁,这个方法就是在 NonfairSync.tryAcquire()下的 **nonfairTryAcquire()**,源码如下:

//一个尝试插队的过程final boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread();//获取state值int c = getState();//比较锁的状态是否为 0,如果是0,当前没有任何一个线程获取锁if (c == 0) {//则尝试去原子抢占这个锁(设置状态为1,然后把当前线程设置成独占线程)if (compareAndSetState(0, acquires)) {// 设置成功标识独占锁setExclusiveOwnerThread(current);return true;}}//如果当前锁的状态不是0 state!=0,就去比较当前线程和占用锁的线程是不是一个线程else if (current == getExclusiveOwnerThread()) {//如果是,增加状态变量的值,从这里看出可重入锁之所以可重入,就是同一个线程可以反复使用它占用的锁int nextc = c + acquires;//重入次数太多,大过Integer.MAXif (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}//如果以上两种情况都不通过,则返回失败falsereturn false;}

tryAcquire() 一旦返回 false,就会则进入 acquireQueued() 流程,也就是基于CLH队列的抢占模式,在CLH锁队列尾部增加一个等待节点,这个节点保存了当前线程,通过调用 addWaiter() 实现,这里需要考虑初始化的情况,在第一个等待节点进入的时候,需要初始化一个头节点然后把当前节点加入到尾部,后续则直接在尾部加入节点。

//AbstractQueuedSynchronizer.addWaiter()private Node addWaiter(Node mode) {// 初始化一个节点,用于保存当前线程Node node = new Node(Thread.currentThread(), mode);// 当CLH队列不为空的视乎,直接在队列尾部插入一个节点Node pred = tail;if (pred != null) {node.prev = pred;//如果pred还是尾部(即没有被其他线程更新),则将尾部更新为node节点(即当前线程快速设置成了队尾)if (compareAndSetTail(pred, node)) {pred.next = node;return node;}}// 当CLH队列为空的时候,调用enq方法初始化队列enq(node);return node;}private Node enq(final Node node) {//在一个循环里不停的尝试将node节点插入到队尾里for (;;) {Node t = tail;if (t == null) { // 初始化节点,头尾都指向一个空节点if (compareAndSetHead(new Node()))tail = head;} else {node.prev = t;if (compareAndSetTail(t, node)) {t.next = node;return t;}}}}

 将节点增加到CLH队列后,进入 acquireQueued() 方法

final boolean acquireQueued(final Node node, int arg) {boolean failed = true;try {boolean interrupted = false;//在一个循环里不断等待前驱节点执行完毕for (;;) {final Node p = node.predecessor();if (p == head && tryAcquire(arg)) {// 通过tryAcquire获得锁,如果获取到锁,说明头节点已经释放了锁setHead(node);//将当前节点设置成头节点p.next = null; // help GC//将上一个节点的next变量被设置为null,在下次GC的时候会清理掉failed = false;//将failed标记设置成falsereturn interrupted;}//中断if (shouldParkAfterFailedAcquire(p, node) && // 是否需要阻塞parkAndCheckInterrupt())// 阻塞,返回线程是否被中断interrupted = true;}} finally {if (failed)cancelAcquire(node);}}

 如果尝试获取锁失败,就会进入 shouldParkAfterFailedAcquire() 方法,会判断当前线程是否阻塞

/*** 确保当前结点的前驱结点的状态为SIGNAL* SIGNAL意味着线程释放锁后会唤醒后面阻塞的线程* 只有确保能够被唤醒,当前线程才能放心的阻塞。*/private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {int ws = pred.waitStatus;if (ws == Node.SIGNAL)//如果前驱节点状态为SIGNAL//表明当前线程需要阻塞,因为前置节点承诺执行完之后会通知唤醒当前节点return true;if (ws > 0) {//ws > 0 代表前驱节点取消了do {node.prev = pred = pred.prev;//不断的把前驱取消了的节点移除队列} while (pred.waitStatus > 0);pred.next = node;} else {//初始化状态,将前驱节点的状态设置成SIGNALcompareAndSetWaitStatus(pred, ws, Node.SIGNAL);}return false;}

进入阻塞阶段,会进入 parkAndCheckInterrupt() 方法,则会调用 LockSupport.park(this) 将当前线程挂起。代码如下:

// 从方法名可以看出这个方法做了两件事
private final boolean parkAndCheckInterrupt() {LockSupport.park(this);//挂起当前的线程// 如果当前线程已经被中断了,返回true,否则返回false// 有可能在挂起阶段被中断了return Thread.interrupted();
}
 3.2.1.2 NonfairSync.unlock()
  1. 调用 unlock() 方法,其实是直接调用 AbstractQueuedSynchronizer.release() 操作。
  2. 进入 release() 方法,内部先尝试 tryRelease() 操作,主要是去除锁的独占线程,然后将状态减一,这里减一主要是考虑到可重入锁可能自身会多次占用锁,只有当状态变成0,才表示完全释放了锁。
  3. 如果 tryRelease 成功,则将CHL队列的头节点的状态设置为0,然后唤醒下一个非取消的节点线程。
  4. 一旦下一个节点的线程被唤醒,被唤醒的线程就会进入 acquireQueued() 代码流程中,去获取锁。
  5. public void unlock() {sync.release(1);
    }public final boolean release(int arg) {//尝试在当前锁的锁定计数(state)值上减1,if (tryRelease(arg)) {Node h = head;if (h != null && h.waitStatus != 0)//waitStatus!=0表明或者处于CANCEL状态,或者是SIGNAL表示下一个线程在等待其唤醒。也就是说waitStatus不为零表示它的后继在等待唤醒。unparkSuccessor(h);//成功返回truereturn true;}//否则返回falsereturn false;
    }private void unparkSuccessor(Node node) {int ws = node.waitStatus;//如果waitStatus < 0 则将当前节点清零if (ws < 0)compareAndSetWaitStatus(node, ws, 0);//若后续节点为空或已被cancel,则从尾部开始找到队列中第一个waitStatus<=0,即未被cancel的节点Node s = node.next;if (s == null || s.waitStatus > 0) {s = null;for (Node t = tail; t != null && t != node; t = t.prev)if (t.waitStatus <= 0)s = t;}if (s != null)LockSupport.unpark(s.thread);}
    

    当然在 release() 方法中不仅仅只是将 state - 1 这么简单,**-1** 之后还需要进行一番处理,如果 -1 之后的 新state = 0 ,则表示当前锁已经被线程释放了,同时会唤醒线程等待队列中的下一个线程。

protected final boolean tryRelease(int releases) {int c = getState() - releases;//判断是否为当前线程在调用,不是抛出IllegalMonitorStateException异常if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();boolean free = false;//c == 0,释放该锁,同时将当前所持有线程设置为nullif (c == 0) {free = true;setExclusiveOwnerThread(null);}//设置statesetState(c);return free;}private void unparkSuccessor(Node node) {int ws = node.waitStatus;if (ws < 0)compareAndSetWaitStatus(node, ws, 0);Node s = node.next;if (s == null || s.waitStatus > 0) {s = null;// 从后往前找到离head最近,而且waitStatus <= 0 的节点// 其实在ReentrantLock中,waitStatus应该只能为0和-1,需要唤醒的都是-1(Node.SIGNAL)for (Node t = tail; t != null && t != node; t = t.prev)if (t.waitStatus <= 0)s = t;}if (s != null) LockSupport.unpark(s.thread);// 唤醒挂起线程
}

重点:unlock最好放在finally中,因为如果没有使用finally来释放Lock,那么相当于启动了一个定时炸弹,如果发生错误,我们很难追踪到最初发生错误的位置,因为没有记录应该释放锁的位置和时间,这也就是 ReentrantLock 不能完全替代 synchronized 的原因,因为当程序执行控制离开被保护的代码块时,不会自动清除锁

3.2.2 公平锁

FairSync相对来说就简单很多,只有重写的两个方法跟NonfairSync不同

inal void lock() {acquire(1);
}protected final boolean tryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {if (!hasQueuedPredecessors() &&// 没有前驱节点了compareAndSetState(0, acquires)) {// 而且没有锁setExclusiveOwnerThread(current);return true;}}else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0)throw new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;
}

3.2.2、公平锁和非公平锁的区别

  • 锁的公平性是相对于获取锁的顺序而言的。
  • 如果是一个公平锁,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是FIFO,线程获取锁的顺序和调用lock的顺序一样,能够保证老的线程排队使用锁,新线程仍然排队使用锁。
  • 非公平锁只要CAS设置同步状态成功,则表示当前线程获取了锁,线程获取锁的顺序和调用lock的顺序无关,全凭运气,也就是老的线程排队使用锁,但是无法保证新线程抢占已经在排队的线程的锁。
  • ReentrantLock默认使用非公平锁是基于性能考虑,公平锁为了保证线程规规矩矩地排队,需要增加阻塞和唤醒的时间开销。如果直接插队获取非公平锁,跳过了对队列的处理,速度会更快。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/779964.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

绿联 部署vocechat,搭建私人聊天服务器,用于小型团队和家庭环境

1、镜像 privoce/vocechat-server:latest 2、安装 2.1、基础设置 重启策略&#xff1a;容器退出时总是重启容器。 2.2、网络 桥接即可。 2.3、存储空间 装载路径&#xff1a;/home/vocechat-server/data不可变更&#xff0c;权限读写。 2.4、端口设置 容器端口3000不可变…

鸿蒙OS开发教学:【编程之重器-装饰器】

HarmonyOS 有19种装饰器 必须【2】 绘制一个页面&#xff0c;这两个肯定会用到 EntryComponent 可选【17】 StatePropLinkObjectLinkWatchStylesStoragePropStorageLinkProvideConsumeObservedBuilderBuilderParamLocalStoragePropLocalStorageLinkExtendConcurrent 如果…

python3将exe 转支持库错误 AssertionError: None does not smell like code

exe -> pyc包(*.exe_extracted) 安装反编译工具 exe反编译工具&#xff1a;pyinstxtractor.py下载&#xff1a;https://sourceforge.net/projects/pyinstallerextractor/ python pyinstxtractor.py hello.exe包反编译 懒的写&#xff01;&#xff01;&#xff01; 这有详…

如何使用Zabbix监控MySQL的MGR群集状态

MySQL的MGR&#xff08;MySQL Group Replication&#xff09;是MySQL官方提供的一种高可用性和高可靠性的集群解决方案。MGR通过使用基于组复制的方式&#xff0c;实现了多个MySQL实例之间的数据同步和故障转移&#xff0c;从而提供了自动故障恢复和负载均衡的功能。本文将介绍…

安装uim-ui插件不成功,成功解决

安装&#xff1a;这种安装&#xff0c;umi4 不支持&#xff0c;只有umi3才支持。而我发现官网现在默认使用的umi4。 yarn add umijs/preset-ui -D 解决&#xff1a;更改umi版本重新安装umi3 npm i ant-design/pro-cli3.1.0 -g #使用umi3 (指定umi3版本) pro create user-ce…

【YOLOv8 代码解读】数据增强代码梳理

1. LetterBox增强 当输入图片的尺寸和模型实际接收的尺寸可能不一致时&#xff0c;通常需要使用LetterBox增强技术。具体步骤是先将图片按比例缩放&#xff0c;将较长的边缩放到设定的尺寸以后&#xff0c;再将较短的边进行填充&#xff0c;最终短边的长度为stride的倍数即可。…

爬虫(Web Crawler)逆向技术探索

实战案例分析 为了更好地理解爬虫逆向的实际应用&#xff0c;我们以一个具体的案例进行分析。 案例背景 假设我们需要从某电商网站上获取商品价格信息&#xff0c;但该网站采取了反爬虫措施&#xff0c;包括动态Token和用户行为分析等。 分析与挑战 动态Token&#xff1a;…

海豚【货运系统源码】货运小程序【用户端+司机端app】源码物流系统搬家系统源码师傅接单

技术栈&#xff1a;前端uniapp后端vuethinkphp 主要功能&#xff1a; 不通车型配置不通价格参数 多城市定位服务 支持发货地 途径地 目的地智能费用计算 支持日期时间 预约下单 支持添加跟单人数选择 支持下单优惠券抵扣 支持司机收藏订单评价 支持订单状态消息通知 支…

Photoshoot 2(Java)

Photoshoot 2 题目描述 在一个似曾相识的场景中&#xff0c;Farmer John 正在将他的 N 头奶牛&#xff08;1≤N≤10^5&#xff09;排成一排&#xff08;为了方便将它们按 1⋯1⋯N 编号&#xff09;&#xff0c;以便拍照。 最初&#xff0c;奶牛从左到右按照 a1,a2,⋯,aN 的顺…

【C/C++】从零开始认识C++历程-启航篇

文章目录 &#x1f4dd;前言&#x1f320; 什么是C&#xff1f;&#x1f309;C的发展史 &#x1f320;C的重要性&#x1f309;语言的使用广泛度 &#x1f320;在工作领域&#x1f309; 岗位需求 &#x1f320;相关笔试题&#x1f309; 公司怎样面试C &#x1f6a9;总结 &#x…

用grafana+prometheus+cadvisor监控容器指标数据,并查询当前容器的网速网络用量

前言 整理技术&#xff0c;在这篇文章中&#xff0c;将会搭建grafanaprometheuscadvisor监控容器&#xff0c;并使用一个热门数据看板&#xff0c;再监控容器的性能指标 dashboard效果 这个是node-exporter采集到的数据&#xff0c;我没装node-exporter&#xff0c;而且这也…

Vitis AI——FPGA学习笔记<?>

参考资料&#xff1a; Xilinx/Vitis-AI-Tutorials (github.com) Xilinx/Vitis-AI: Vitis AI is Xilinx’s development stack for AI inference on Xilinx hardware platforms 【03】ALINX Zynq UltraScale MPSoC XILINX FPGA视频教程Vitis AI开发 一. 简介 1.简介 边缘计…

【物联网】Qinghub opc-ua 连接协议

基础信息 组件名称 &#xff1a; opcua-connector 组件版本&#xff1a; 1.0.0 组件类型&#xff1a; 系统默认 状 态&#xff1a; 正式发布 组件描述&#xff1a;通过OPCUA连接网关&#xff0c;通过定时任务获取OPCUA相关的数据或通过执行指令控制设备相关参数。 配置文件&a…

卸载原有的cuda,更新cuda

概述&#xff1a;看了一下自己的gpu&#xff0c;发现驱动可能装低了&#xff0c;随即尝试更新驱动&#xff0c;写下此篇 注&#xff1a;我原先是10.2的版本&#xff0c;改了之后是11.2&#xff0c;下面的图都用11.2的&#xff0c;不过不碍事 目录 第一步&#xff1a;查看现在…

位运算算法(2)

目录 面试题 01.01. 判断字符是否唯一 一、题目描述 二、思路解析 三、代码 268.丢失的数字 一、题目描述 二、思路解析 三、代码 371.两整数之和 一、题目描述 二、思路解析 三、代码 137.只出现一次的数字 II 一、题目描述 二、思路解析 三、代码 面试题 01.0…

Web漏洞-深入WAF注入绕过

目录 简要其他测试绕过 方式一:白名单&#xff08;实战中意义不大&#xff09; 方式二:静态资源 方式三: url白名单 方式四:爬虫白名单 #阿里云盾防SQL注入简要分析 #安全狗云盾SQL注入插件脚本编写 在攻防实战中&#xff0c;往往需要掌握一些特性&#xff0c;比如服务…

下拉选中搜索angularjs-dropdown-multiselect.js

需要引入angularjs-dropdown-multiselect.js 页面 <div ng-dropdown-multiselect"" options"supplierList_data" selected-model"supplierList_select" events"changSelValue_supplierList" extra-settings"mucommonsetti…

四、Yocto创建静态IP和VLAN(基于raspiberrypi 4B)

Yocto创建VLAN配置 在车载域控中很多时候需要创建VLAN&#xff0c;本小节记录如何为yocto构建出来的image自动化创建静态IP以及VLAN。 关于各种VLAN的配置参考&#xff1a;VLAN 1. ubuntu系统中使用netplan创建VLAN 正常情况下我们在ubuntu系统中可以通过netplan来自动化创建…

【Web自动化】Selenium的使用(一)

目录 关于自动化测试selenium工作机制 selenium的使用selenium中常用API定位元素按id定位按名称定位按类名定位按标签名定位按CSS选择器定位按XPath定位示例 操作测试对象等待sleep休眠隐式等待显示等待 打印信息浏览器操作键盘事件鼠标事件切换窗口截图关闭浏览器 欢迎阅读本文…