彻底理解Java并发:乐观锁、悲观锁和CAS

一、悲观锁与乐观锁

锁的一种宏观分类方式是悲观锁和乐观锁。悲观锁与乐观锁并不是特指某个锁(Java 中没有哪个 Lock 实现类就叫 PessimisticLock 或 OptimisticLock),而是在并发情况下的两种不同策略。

1、乐观锁(Optimistic Lock)
乐观锁认为自己在使用数据的时候,不会有别的线程修改数据,所以不会加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据

锁实现:CAS 算法,例如 Java 原子类 AtomicInteger 的自增是通过 CAS自 旋实现

使用场景:读操作较多,不加锁的特点能使其读操作的性能大幅提升


2、悲观锁(Pessimistic Lock)
悲观锁认为自己在使用数据时一定有别的线程来修改数据,在获取数据时会先加锁,确保数据不会被其他线程修改

锁实现:关键字 syncchronized 、接口 lock 的实现类

使用场景:写操作较多,先加锁可以保证写操作时的数据正确


悲观锁阻塞事务,乐观锁回滚重试,它们各有优缺点,不要认为一种一定好于另一种。像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行重试,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。

Java 中的并发锁大致分为隐式锁和显式锁两种。隐式锁就是我们最常使用的 synchronized 关键字,显式锁主要包含两个接口:Lock 和 ReadWriteLock,主要实现类分别为 ReentrantLock 和 ReentrantReadWriteLock,这两个类都是基于 AQS(AbstractQueuedSynchronizer) 实现的。还有的地方将 CAS 也称为一种锁,在包括 AQS 在内的很多并发相关类中,CAS 都扮演了比较重要的角色。

二、CAS(Compare And Swap)

1、比较并交换
CAS,即「比较并交换」。java.util.concurrent 包中借助 CAS 实现了区别于 synchronouse 同步锁的一种乐观锁。CAS 是解决多线程并行情况下使用锁造成性能损耗的一种机制。

CAS 操作包含三个操作数——内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。CAS 有效地说明了:“我认为位置V应该包含值A;如果包含该值,则将B放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可“。

在 JAVA 中,sun.misc.Unsafe 类提供了硬件级别的原子操作来实现这个 CAS。java.util.concurrent 包下的大量类都使用了这个 Unsafe.java 类的 CAS 操作。

2、非阻塞算法
一个线程的失败或者挂起不应该影响其他线程的失败或挂起的算法,下面来看一下原子操作(AtomicInteger Jdk1.5)的 ++i 是如何借助 CAS 实现的:

public final int incrementAndGet() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return next;
    }
}
public final boolean compareAndSet(int expect, int update) {   
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

其中,compareAndSwapInt 是借助 C 来调用 CPU 底层指令实现的。

3、ABA问题
因为 CAS 需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是 A,变成了 B,又变成了 A,那么使用 CAS 进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA 问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么 A-B-A 就会变成 1A-2B-3A

AtomicStampedReference 里面增加了一个时间戳,也就是说每一次修改只需要设置不同的版本号以解决 ABA 问题。

此外CAS自旋时,如果长时间不成功,就会带来循环时间长,开销大的问题。

三、对 Java 中 CAS 的实现解读

1、AtomicInteger 对 CAS 的实现
AtomicInteger 是一个支持原子操作的 Integer 类,就是保证对 AtomicInteger 类型变量的增加和减少操作是原子性的,不会出现多个线程下的数据不一致问题。如果不使用 AtomicInteger,要实现一个按顺序获取的 ID,就必须在每次获取时进行加锁操作,以避免出现并发时获取到同样的 ID 的现象。

接下来通过源代码来看 Jdk8 中 AtomicInteger 中 incrementAndGet() 方法的实现,下面是具体的代码。

// JDK 8
public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

在 incrementAndGet 里,我们可以看到使用了 Unsafe 类,下面是 Unsafe 里提供的 getAndAddInt 方法:

public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
          v = getIntVolatile(o, offset);
           } while (!compareAndSwapInt(o, offset, v, v + delta));
    return v;
}

通过一个 do while 语句来做一个主体实现的在 while 语句里核心调了 compareAndSwapInt() 方法:

public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);

此方法为 native 方法,compareAndSwapInt 基于的是 CPU 的 CAS 指令来实现的。所以基于 CAS 的操作可认为是无阻塞的,一个线程的失败或挂起不会引起其它线程也失败或挂起。并且由于 CAS 操作是 CPU 原语,所以性能比较好。

回到 incrementAndGet 中:我们传过来的第一个值是当前的对象,第二个值是我们当前的值(比如如果我们要实现2+1)那么 offset 就是 2 delta 就是1,这里的 v,它是我们调用底层的方法v v = this.getIntVolatile(o, offset); 获取底层当前的值。如果没有其他线程来处理 o 这个变量的时候,它的正常返回值应该是 2,因此传到 compareAndSwapInt 的参数就是(o,2,2,2+1),这个方法想达到的目标就是对于 o 这个对象,如果当前的这个值和底层的这个值相等的情况下,就把它更新成后面那个值 v + delta。

当我们一个方法进来的时候,我们 offset 的值是2,我们第一次取出来 v 的值也等于 2,但是当我们在执行更新成 3 的时候 也就是这句代码 while (!compareAndSwapInt(o, offset, v, v + delta));可能会被其它线程更改,所以我们要判断 offset 是否与 v 是相同的,只有是相同的,才允许它更新为 3。通过这样不停的循环来判断。就能保证期望的值和底层的值相同。

CAS比较与交换的伪代码可以表示为:

do{
        备份旧数据;
        基于旧数据构造新数据;
}while(!CAS( 内存地址,备份的旧数据,新数据 ))

Java中的乐观锁大部分都是基于CAS(Compare And Swap,比较和交换)操作实现的,CAS设一种原子操作,在对数据操作之前,首先会比较当前值跟传入值是否一样,如果一样咋更新,否则不执行更新操作直接返回失败状态。compareAndSwapInt 也是 CAS 的核心。

2、Unsafe 类简介
Unsafe 类和 C++ 有点类似,在 Java 中是没有办法直接操作内存的,但是 Unsafe 类却可以间接的让程序员操作内存区域。

Unsafe 是位于 sun.misc 包下的一个类。Unsafe 提供的 API 大致可分为内存操作、CAS、Class 相关、对象操作、线程调度、系统信息获取、内存屏障、数组操作等几类。由于并发相关的源码很多用到了 CAS,比如 java.util.concurrent.atomic 相关类、AQS、CurrentHashMap 等相关类。

CAS 主要相关源码:

    /**
     * 参数说明
     * @param o             包含要修改field的对象
     * @param offset        对象中某个参数field的偏移量,该偏移量不会改变
     * @param expected      期望该偏移量对应的field值
     * @param x             更新值
     * @return              true|false
     */
    public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object x);

    public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);

    public final native boolean compareAndSwapLong(Object o, long offset, long expected, long x);

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

RK3568平台 USB数据包的收发格式

一.USB硬件拓扑结构 compound device :多个设备组合起来,通过HUB跟Host相连composite device :一个物理设备有多个逻辑设备(multiple interfaces) 在软件开发过程中,我们可以忽略Hub的存在,硬件拓扑图简化如下&#x…

git revert 撤回之前的几个指定的提交

文章目录 Intro操作命令-n 选项 参考 Intro 在开发过程中,有的时候一开始只是一个小需求,可以改着改着事情超出了控制,比如说我一开始只是想调整一个依赖包的版本,可是改到后来类库不兼容甚至导致项目无法启动。 这个时候我就想&…

(二十二)devops持续集成开发——jenkins服务代理Agent搭建

前言 在Jenkins 中,代理(Agent)是一种用于执行构建、部署和其他任务的计算节点。代理节点可以是物理机器、虚拟机或容器,它们负责接收 Jenkins 主控节点委派的任务并执行这些任务。通过使用代理节点,可以有效地分担Je…

基于WebDriverAgent代理服务,实现iOS手机app自动化测试的框架搭建

🔥 交流讨论:欢迎加入我们一起学习! 🔥 资源分享:耗时200小时精选的「软件测试」资料包 🔥 教程推荐:火遍全网的《软件测试》教程 📢欢迎点赞 👍 收藏 ⭐留言 &#x1…

[数据结构初阶]队列

鼠鼠我呀,今天写一个基于C语言关于队列的博客,如果有兴趣的读者老爷可以抽空看看,很希望的到各位老爷观点和点评捏! 在此今日,也祝各位小姐姐女生节快乐啊,愿笑容依旧灿烂如初阳,勇气与童真永不…

每日五道java面试题之springMVC篇(一)

目录: 第一题. 什么是Spring MVC?简单介绍下你对Spring MVC的理解?第二题. Spring MVC的优点第三题. Spring MVC的主要组件?第四题. 什么是DispatcherServlet?第五题. 什么是Spring MVC框架的控制器? 第一题. 什么是S…

JavaScript 作用域详解:如何影响变量生命周期

🤍 前端开发工程师、技术日更博主、已过CET6 🍨 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 🕠 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 🍚 蓝桥云课签约作者、上架课程《Vue.js 和 E…

Linux系统——Keepalive群集部署及认识

目录 一、Keepalive的认识 1.Keepalive基础——VRRP 2.Keepalived工具介绍 2.1Keepalived介绍 2.2Keepalived架构 2.2.1用户空间核心组件 2.2.2WatchDog:监控进程(整个架构是否有问题) 二、安装Keepalived及相关配置文件详解 1.安装…

python 输入和输出

在 Python 中,输入和输出是最基本的操作之一。你可以使用内置函数 input() 来获取用户输入,使用 print() 函数来输出信息到控制台。 输入(Input) input() 函数用于从用户那里获取输入。这个函数会将用户的输入作为字符串返回。 示…

【C语言】终の指针(前篇)

个人主页点这里~ 指针初阶点这里~ 指针初阶2.0点这里~ 指针进阶点这里~ 终の指针 一、回调函数二、qsort函数1、整形比较2、结构数据比较①结构体②-> 的使用③结构数据比较 一、回调函数 回调函数就是⼀个通过函数指针调用的函数。 把一个函数的指针作为参数传递给另一…

分类预测 | Matlab基于GWO-RBF灰狼算法优化径向基神经网络的分类预测

分类预测 | Matlab基于GWO-RBF灰狼算法优化径向基神经网络的分类预测 目录 分类预测 | Matlab基于GWO-RBF灰狼算法优化径向基神经网络的分类预测分类效果基本介绍程序设计参考资料 分类效果 基本介绍 Matlab基于GWO-RBF灰狼算法优化径向基神经网络的分类预测。基于灰狼算法(GWO…

和为K的子数组

题目: 使用前缀和的方法可以解决这个问题,因为我们需要找到和为k的连续子数组的个数。通过计算前缀和,我们可以将问题转化为求解两个前缀和之差等于k的情况。 假设数组的前缀和数组为prefixSum,其中prefixSum[i]表示从数组起始位…

仓储管理系统(WMS) 的研发历程-PRD撰写

题外话:PRD的展现形式有多种,有的人喜欢在axure上直接做产品描述,觉得word较为过时,有的人认为axure不专业,任何展现形式都无可厚非,重要的达到PRD的目的,PRD的目标是让团队知道需求实现细节&am…

Vue中如何处理用户权限?

在前端开发中,处理用户权限是非常重要的一个方面。Vue作为一种流行的前端框架,提供了很多便捷的方式来管理用户权限。本文将介绍一些Vue中处理用户权限的方法 1. 使用路由守卫 Vue Router提供了一个功能强大的功能,即导航守卫(N…

18-Java迭代器模式 ( Iterator Pattern )

Java迭代器模式 摘要实现范例 迭代器模式(Iterator Pattern)用于顺序访问集合对象的元素,不需要知道集合对象的底层表示 迭代器模式是 Java 和 .Net 编程环境中非常常用的设计模式 迭代器模式属于行为型模式 摘要 1. 意图 提供一种方法…

【LeetCode:2917. 找出数组中的 K-or 值 + 模拟+位运算】

🚀 算法题 🚀 🌲 算法刷题专栏 | 面试必备算法 | 面试高频算法 🍀 🌲 越难的东西,越要努力坚持,因为它具有很高的价值,算法就是这样✨ 🌲 作者简介:硕风和炜,…

国内鞋服品牌如何打造出优衣库的“零库存”运营体系

优衣库,作为全球知名的服装品牌,以其独特的“零库存”运营体系在业界树立了标杆。对于国内鞋服品牌而言,如何借鉴并打造类似的“零库存”运营体系,不仅是提升竞争力的关键,也是实现可持续发展的必然选择。本文将探讨国…

springboot实现多线程开发(使用@Async注解,简单易上手)

根据springboot的核心思想便捷开发,使用多线程也变得简单起来,通过一下几个步骤即可实现。 核心注解 EnableAsync将此注解加在启动类上,使项目支持多线程。 Async 使用我们的Async注解在所需要进行多线程的类上即可实现。 配置线程池 …

2024/3/7—2575. 找出字符串的可整除数组

代码实现&#xff1a; int* divisibilityArray(char *word, int m, int *returnSize) {int n strlen(word);int *res (int*)malloc(sizeof(int) * n);long cur 0;for (int i 0; i < n; i) {cur (cur * 10 (word[i] - 0)) % m;res[i] (cur 0) ? 1 : 0;}*returnSize …

1.BOM-获取元素(获取元素、修改属性)

web Api基本认知 作用&#xff1a;通过JS去操作html页面和浏览器(实现浏览器中的某些功能) 分类&#xff1a; DOM(网页)&#xff1a;Document Object Model(文档对象模型) BOM(浏览器)&#xff1a;Borwser Object Model(浏览器对象模型) DOM DOM树 将网页中标签的关系以树状…