彻底理解Java并发:乐观锁与CAS

news2024/12/21 8:02:51

本篇内容包括:悲观锁与乐观锁的概述、CAS(Compare And Swap)比较并交换的介绍、非阻塞算法与ABA问题,以及对 Java 中 CAS 的实现解读(AtomicInteger 对 CAS 的实现,Unsafe 类简介)。

一、悲观锁与乐观锁

锁的一种宏观分类方式是悲观锁和乐观锁。悲观锁与乐观锁并不是特指某个锁(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);

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

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

相关文章

【树莓派不吃灰】Raspberry Pi上搭建NodeJS运行环境

目录1. 前言2. 安装NodeJS环境2.1 安装npm2.2 安装nodejs2.3 配置NPM国内镜像源3. 总结❤️ 博客主页 单片机菜鸟哥,一个野生非专业硬件IOT爱好者 ❤️❤️ 本篇创建记录 2022-10-28 ❤️❤️ 本篇更新记录 2022-10-28 ❤️🎉 欢迎关注 🔎点赞…

嵌入式C语言编程中经验教训总结(八)变量、指针和指针数组的内存管理

目录嵌入式C语言编程中经验教训总结(八)变量、指针和指针数组的内存管理变量、指针和指针数组的内存占用指针、指针数组的空间验证指针数组的元素数据访问方法嵌入式C语言编程中经验教训总结(八)变量、指针和指针数组的内存管理 …

【趣学算法】第一章读书笔记

14天阅读挑战赛 *努力是为了不平庸~ 算法学习有些时候是枯燥的,这一次,让我们先人一步,趣学算法! 文章目录1.1打开算法之门1.2 妙不可言——算法复杂性算法的特性好算法的标准时间复杂度和空间复杂度时间复杂度空间复杂度宕机1.4算…

62. 如何通过增强(Enhancement) 的方式给 SAP ABAP 标准程序增添新功能

文章目录 如何找到可以创建增强实现的增强点位置如何创建增强实现如何在 SE80 里找到增强实现本身如何调试 ABAP 增强实现总结ABAP 系统有比较完善的修改控制权限管控,比如笔者试图修改一个 SAP ABAP 系统里标准的函数,就会遇到如下的警告消息,然后修改的尝试会被阻止: You…

Winform和ASP.NET、Web API详解

Winform和ASP.NET、Web API 一、winform基础 1.1 基础学习 1、 winform应用程序是一种智能客户端技术,我们可以使用winform应用程序帮助我们获得信息或者传输信息等。 2、属性 Name:在后台要获得前台的控件对象,需要使用Name属性。 visible:指示一…

认识运营商机房

文章目录走线设备机房走线数据机房走线传输机房列头柜【供电】网络架构ONU设备OLT设备汇聚层交换机BARS设备核心路由器运营商网络架构【必看】铁塔基站核心机房ODF:光纤配线架MME光纤SGWPGWHSS交换机拓扑核心机房拓扑接入层基站(BaseStation)…

山西大同大学技术会,大同大学的家!

大家好,我是康来个程,山西大同大学技术会的创建者。 低谷时代 近几年校内的竞赛氛围越来越浓厚,随着自身参与并了解的赛事越来越多,随之而来的也是发现了我们学校竞赛方面的问题。疫情原因,我们的比赛取消的取消&…

Gitee在大数据中心的使用

在本地主机或者可以VSCode直接连接可视化的服务器上 1. 首先在gitee新建一个带有develop分支的仓库 2. 在自己的主机(e.g., server 1~3)上git clone下来,例如 git clone gitgitee.com:PeterBishop0/TransT-based.git 3. 切换成develop分支&…

深度学习入门(十) 模型选择、过拟合和欠拟合

深度学习入门(十) 模型选择、过拟合和欠拟合前言模型选择例子:预测谁会偿还贷款?训练误差和泛化误差验证数据集和测试数据集K-则交叉验证总结过拟合和欠拟合模型容量模型容量的影响估计模型容量VC维线性分类器的VC维VC维的用处数据…

[云原生之k8s] Kubernetes原理

引言 单机容器编排:docker-compose 容器集群编排:docker swarm、mesosmarathon、kubernetes 应用编排:ansible 一、Kubernetes是什么? Kubernetes的缩写为:K8S,这个缩写是因为k和s之间有八个字符的关系…

线段树模板

好文分享:【数据结构】线段树(Segment Tree) - 小仙女本仙 - 博客园 线段树和树状数组的基本功能都是在某一满足结合律的操作(比如加法,乘法,最大值,最小值)下,O(logn)的时间复杂度内修改单个元…

Python回归预测建模实战-支持向量机预测房价(附源码和实现效果)

机器学习在预测方面的应用,根据预测值变量的类型可以分为分类问题(预测值是离散型)和回归问题(预测值是连续型),前面我们介绍了机器学习建模处理了分类问题(具体见之前的文章)&#…

x86 --- 任务隔离特权级保护

程序是记录在载体上的数据和指令。 程序正在执行时的一个副本叫做任务 所有段描述符都放在GDT --> 不做区分。 内核程序(任务)所占段在GDT中,用户程序(任务)所占段在LDT中 --> 做区分。 每个任务都有自己独立的…

【无标题】

第1章 概述 本章主要内容: 互联网的概念(标准化)、组成、发展历程;电路交换的基本概念、分组交换的原理;计算机网络的分类、性能指标及两种体系结构。 重点掌握: 在计算机网络分层模型中,网…

7、GC日志详解

目录如何分析GC日志参数配置程序运行GC日志打印解析GC日志数据分析指定其他垃圾收集器CMSG1GC分析工具JVM参数汇总查看命令如何分析GC日志 参数配置 对于java应用我们可以通过一些配置把程序运行过程中的gc日志全部打印出来,然后分析gc日志得到关键性指标&#xff…

目标检测算法——遥感影像数据集资源汇总(附下载链接)

关注”PandaCVer“公众号 深度学习资料,第一时间送达 目录 一、用于 2-5 分类问题 1.UCAS-AOD 遥感影像数据集 2.Inria Aerial Image Labeling Dataset 3.RSOD-Dataset 物体检测数据集 二、用于 5-10 分类问题 1.RSSCN7 DataSet 遥感图像数据集 2.NWPU…

孙宇晨接受韩国媒体专访:熊市受宏观经济的不确定性影响

10月27日至10月29日,韩国釜山备受关注的大型区块链活动 2022 釜山区块链周(BWB 2022)在釜山会展中心(BEXCO)举行。韩国区块链媒体TokenPost 对出席活动的波场TRON创始人孙宇晨进行了专访。10月28日,该媒体发…

Nginx快速入门部署前端项目

目录 一,Nginx简介 1.1 负载均衡 演示 1.1.2 安装nginx 再复制一份一样的tomcat并修改端口号 打开两个tomcat的服务 打开防火墙中的8081端口 修改Nginx配置 重启Nginx服务,让配置生效 1.2 反向代理 Nginx项目部署 1.确保前端项目能用 2.将前台项目…

看过来,Windows 11 Insider Preview 25231.1000推送啦!

微软于近日凌晨发布新的Windows 11内部预览版系统,版本号为25231.1000,该系统对平板任务栏体验进行了改进,修复了系统托盘、设置等问题。下面一起来看看完整的更新内容。 更新日志 TL;速度三角形定位法(dead reckoning…

【ASM】字节码操作 转换已有的类 清空方法体

1.概述 在文章:【ASM】字节码操作 转换已有的类 移除Instruction 移除NOP 中我们学会了如何移除NOP。 本章我们将学习如何清空方法体。 1.1 如何清空方法体 在有些情况下,我们可能想清空整个方法体的内容,那该怎么做呢?其实,有两个思路。 ●第一种思路,就是将instructi…