Java高并发核心编程—CAS与JUC原子类

news2025/1/11 14:44:46

注:本笔记是阅读《Java高并发核心编程卷2》整理的笔记!

CAS原理
JUC原子类一Atomic
基本原子类
数组原子类
引用原子类
字段更新原子类
AtomicInteger 线程安全原理
引用类型原子类
属性更新原子类
ABA问题
提升高并发场景下CAS提作的性能
以空间换时间: LongAdder
CAS优势与弊端
提升CAS性能

JVM的synchronized轻量级锁使用CAS(Compare And Swap,比较并交换)进行自旋抢锁, CAS是CPU指令级的原子操作并处于用户态下,所以JVM轻量级锁开销较小。 在java.util.concurrent.atomic包的原子类(如AtomicXXX中)都使用了CAS保障对数字成员进行操作的原子性。java.util.concurrent的大多数类(包括显式锁、并发容器)都是基于AQS和AtomicXXX实现的,其中AQS通过CAS保障其内部双向队列队头、队尾操作的原子性

CAS原理

JDK 5所增加的JUC(java.util.concurrent)并发包对操作系统的底层CAS原子操作进行了封装,为上层Java程序提供了CAS操作的API。 CAS是一种无锁算法,该算法关键依赖两个值——期望值(就值)和新值,底层CPU利用原子操作判断内存原值与期望值是否相等,如果相等就给内存地址赋新值,否则不做任何操作。

使用CAS进行无锁编程的步骤大致如下:

  1. 获得字段的期望值(oldValue)。
  2. 计算出需要替换的新值(newValue)。
  3. 通过CAS将新值(newValue)放在字段的内存地址上,如果CAS失败就重复第1)步到第2)步,直到CAS成功,这种重复俗称CAS自旋。
do
{
    获得字段的期望值(expValue),也就是读取内存原值;
    计算出需要替换的新值(newValue);
} while (!CAS(内存地址, expValue, newValue)) // 判断期望值是否等于现在的内存原值,如果等于表示此期间没人修改,否则就被修改了,重新获得期望值。

假设共享变量V内存值为100,线程A对V减1操作,线程B对V加1操作。使用CAS实现:无论A、B想要对其修改都先获取V的值,作为期望值,然后才进行操作,最后提交修改的时候需要用开始的期望值去与内存原值V进行对比,如果相等表示未被修改。如果不相等表示已被修改了,重新进行前面操作,获取V值作为期望值。。。

当并发修改的线程少,冲突出现的机会少时,自旋的次数也会很少, CAS性能会很高;当并发修改的线程多,冲突出现的机会多时,自旋的次数也会很多, CAS性能会大大降低。所以,提升CAS无锁编程效率的关键在于减少冲突的机会。 所以CAS适合用于并发线程数较少的场景。

CAS 必须借助 volatile 才能读取到共享变量的最新值来实现比较并交换的效果

可以使用Unsafe类实现CAS原子操作:

//通过CAS原子操作,进行“比较并交换”
public final boolean unSafeCompareAndSet(int oldValue, int newValue)
{   //valueOffset:偏移量表示该变量值相对于当前对象地址的偏移,Unsafe 就是根据内存偏移地址获取数据。原子操作:使用unsafe的“比较并交换”方法进行value属性的交换
	return unsafe.compareAndSwapInt( this, valueOffset,oldValue ,newValue );
}

JUC原子类—Atomic

在多线程并发执行时,诸如“++”或“–”类的运算不具备原子性,不是线程安全的操作。通常情况下,大家会使用synchronized将这些线程不安全的操作变成同步操作,但是这样会降低并发程序的性能。所以, JDK为这些类型不安全的操作提供了一些原子类,与synchronized同步机制相比, JDK原子类基于CAS轻量级原子操作实现,使得程序运行效率变得更高。

JUC并发包中原子类都存放在java.util.concurrent. atomic类路径下,可以将JUC包中的原子类分为4类:基本原子类、数组原子类、原子引用类和字段更新原子类。 主要使用的为基本原子类和数组原子类,其他稍作了解。

基本原子类

基本原子类的功能是通过原子方式更新Java基础类型变量的值 :

  • AtomicInteger:整型原子类。
  • AtomicLong:长整型原子类。
  • AtomicBoolean:布尔型原子类。

在多线程环境下,如果涉及基本数据类型的并发操作,不建议采用synchronized重量级锁进行线程同步,而是建议优先使用基础原子类保障并发操作的线程安全性。 基础原子类AtomicInteger常用的方法主要如下:

public final int get() //获取当前的值
public final int getAndSet(int newValue) //获取当前的值,然后设置新的值
public final int getAndIncrement() //获取当前的值,然后自增
public final int getAndDecrement() //获取当前的值,然后自减
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update); //通过CAS方式设置整数值

int tempvalue = 0;
AtomicInteger i = new AtomicInteger(0);
//取值,然后设置一个新值 i=3
tempvalue = i.getAndSet(3);
//取值,然后自增 i=4
tempvalue = i.getAndIncrement(); 
//取值,然后增加5 i=9
tempvalue = i.getAndAdd(5);
//CAS交换 i=100
boolean flag = i.compareAndSet(9, 100);
数组原子类

数组原子类的功能是通过原子方式更新数组中的某个元素的值 :

  • AtomicIntegerArray:整型数组原子类。
  • AtomicLongArray:长整型数组原子类。
  • AtomicReferenceArray:引用类型数组原子类

上面三个类提供的方法几乎相同,所以我们这里以AtomicIntegerArray为例来介绍。AtomicIntegerArray类常用方法如下:

//获取 index=i 位置元素的值
public final int get(int i)
//返回index=i位置的当前的值,并将其设置为新值: newValue
public final int getAndSet(int i, int newValue)
//获取index=i位置元素的值,并让该位置的元素自增
public final int getAndIncrement(int i)
//获取index=i位置元素的值,并让该位置的元素自减
public final int getAndDecrement(int i)
//获取index=i位置元素的值,并加上预期的值
public final int getAndAdd(int delta)
//如果输入的数值等于预期值,就以原子方式将位置i的元素值设置为输入值(update)
boolean compareAndSet(int expect, int update)
//最终将位置i的元素设置为newValue
//lazySet方法可能导致其他线程在之后的一小段时间内还是可以读到旧的值
public final void lazySet(int i, int newValue);

int tempvalue = 0;
//原始的数组
int[] array = { 1, 2, 3, 4, 5, 6 };
//包装为原子数组
AtomicIntegerArray i = new AtomicIntegerArray(array);
//获取第0个元素,然后设置为2 ,输出 tempvalue:1; i:[2, 2, 3, 4, 5, 6] 
tempvalue = i.getAndSet(0, 2);
//获取第0个元素,然后自增,输出tempvalue:2; i:[3, 2, 3, 4, 5, 6]
tempvalue = i.getAndIncrement(0); 
//获取第0个元素,然后增加一个delta 5,输出tempvalue:3; i:[8, 2, 3, 4, 5, 6]
tempvalue = i.getAndAdd(0, 5); 
引用原子类

引用原子类主要包括以下三个:

  • AtomicReference:引用类型原子类。
  • AtomicMarkableReference:带有更新标记位的原子引用类型。
  • AtomicStampedReference:带有更新版本号的原子引用类型。
字段更新原子类

字段更新原子类主要包括以下三个:

  • AtomicIntegerFieldUpdater:原子更新整型字段的更新器
  • AtomicLongFieldUpdater:原子更新长整型字段的更新器。
  • AtomicReferenceFieldUpdater:原子更新引用类型里的字段。
AtomicInteger 线程安全原理

基础原子类(以AtomicInteger为例)主要通过CAS自旋+volatile相结合的方案实现,既保障了变量操作的线程安全性,又避免了synchronized重量级锁的高开销,使得Java程序的执行效率大为提升。 CAS用于保障变量操作的原子性, volatile关键字用于保障变量的可见性(即一个线程修改了某个volatile变量的值,该值对其他线程立即可见。),二者常常结合使用。

源代码实现:

//Unsafe类实例,也是使用Unsafe类的CAS操作
private static final Unsafe unsafe = Unsafe.getUnsafe();
//内部value值,使用volatile保证线程可见性
private volatile int value;
//对比expect(期望值)与value,若不同则返回false
//若expect与value相同,则将新值赋给value,并返回true,否则循环自旋,直到成功
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
引用类型原子类

基础的原子类型只能保证一个变量的原子操作,当需要对多个变量进行操作时, CAS无法保证原子性操作,这时可以用AtomicReference(原子引用类型)保证对象引用的原子性。简单来说,如果需要同时保障对多个变量操作的原子性,就可以把多个变量放在一个对象中进行操作。

使用原子引用类型AtomicReference包装了User对象之后,只能保障User引用的原子操作,对被包装的User对象的字段值修改时不能保证原子性,这点要切记。

这里以AtomicReference为例子来介绍 ,首先定义一个User类,属性包括 uid,nickName,age三个。

public class User implements Serializable{
    String uid; //用户ID
    String nickName; //昵称
    public volatile int age; //年龄
    public User(String uid, String nickName){
        this.uid = uid;
        this.nickName = nickName;
    }
    @Override
    public String toString(){
        return "User{" +
            "uid='" + getUid() + '\'' +
            ", nickName='" + getNickName() + '\'' +
            ", platform=" + getPlatform() +
            '}';
    }
}

使用AtomicReference对User的引用进行原子性修改,代码如下:

//包装的原子对象
AtomicReference<User> userRef = new AtomicReference<User>();
//待包装的User对象
User user = new User("1", "张三");
//为原子对象设置值
userRef.set(user); 
//要使用CAS替换的User对象
User updateUser = new User("2", "李四");
//使用CAS替换 , 成功success为true,user为李四
boolean success = userRef.compareAndSet(user, updateUser); 
属性更新原子类

如果需要保障对象某个字段(或者属性)更新操作的原子性,需要用到属性更新原子类。这里以AtomicIntegerFieldUpdater为例来介绍,使用属性更新原子类保障属性安全更新的流程大致需要两步:

  • 第一步,更新的对象属性必须使用public volatile修饰符。
  • 第二步,因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法newUpdater()创建一个更新器,并且需要设臵想要更新的类和属性。
//使用静态方法newUpdater()创建一个更新器updater
AtomicIntegerFieldUpdater<User> updater=
AtomicIntegerFieldUpdater.newUpdater(User.class, "age");
User user = new User("1", "张三");
//使用属性更新器的getAndIncrement、 getAndAdd增加user的age值
Print.tco(updater.getAndIncrement(user)); // 1
Print.tco(updater.getAndAdd(user, 100)); // 101
//使用属性更新器的get获取user的age值
Print.tco(updater.get(user)); // 101
ABA问题

比如一个线程A从内存位置M中取出V1,另一个线程B也取出V1。现在假设线程B进行了一些操作之后将M位置的数据V1变成了V2,然后又在一些操作之后将V2变成V1。之后,线程A进行CAS操作,但是线程A发现M位置的数据仍然是V1,最后线程A操作成功。尽管线程A的CAS操作成功,但是不代表这个过程是没有问题的,线程A操作的数据V1可能已经不是之前的V1,而是被线程B替换过的V1,这就是ABA问题。

举例:假设共享变量V内存值为100,线程A对V减1再加1操作,线程B对V加1再减1操作。使用CAS实现:无论A、B想要对其修改都先获取V的值,作为期望值,然后才进行操作,最后提交修改的时候需要用开始的期望值去与内存原值V进行对比,如果相等表示未被修改。如果不相等表示已被修改了。在这种情况下,无论A、B线程谁先读取并修改,之后的线程总能不通过自旋修改成功!即好像这个乐观锁不存在似的!

解决方案:

使用乐观锁的版本号方式,乐观锁每次在执行数据的修改操作时都会带上一个版本号,版本号和数据的版本号一致就可以执行修改操作并对版本号执行加1操作,否则执行失败。因为每次操作的版本号都会随之增加,所以不会出现ABA问题,因为版本号只会增加不会减少。

JDK提供了一个类似AtomicStampedReference类来解决ABA问题。AtomicStampReference在CAS的基础上增加了一个Stamp(印戳或标记),使用这个印戳可以用来觉察数据是否发生变化,给数据带上了一种实效性的检验。 AtomicStampReference的compareAndSet()方法首先检查当前的对象引用值是否等于预期引用,并且当前印戳标志是否等于预期标志,如果全部相等,就以原子方式将引用值和印戳标志的值更新为给定的更新值。

//compareAndSet方法的第一个参数是原CAS中的原参数,第二个参数是要替换后的新参数,第
//三个参数是原来CAS数据旧的版本号,第四个参数表示替换后的版本号。
public boolean compareAndSet(V expectedReference, //预期引用值
                             V newReference, //更新后的引用值
                             int expectedStamp, //旧的版本号
                             int newStamp) //新的版本号,+1即可

提升高并发场景下 CAS 操作的性能

在争用激烈的场景下,会导致大量的CAS空自旋。比如,在大量的线程同时并发修改一个AtomicInteger时,可能有很多线程会不停地自旋,甚至有的线程会进入一个无限重复的循环中。大量的CAS空自旋会浪费大量的CPU资源,大大降低了程序的性能。大量的CAS操作还可能导致“总线风暴” 。在高并发场景下如何提升CAS操作性能呢?可以使用LongAdder替代AtomicInteger。

以空间换时间: LongAdder

Java 8提供一个新的类LongAdder,以空间换时间的方式提升高并发场景下CAS操作性能。LongAdder核心思想就是热点分离,与ConcurrentHashMap的设计思想类似:将value值分离成一个数组,当多线程访问时,通过Hash算法将线程映射到数组的一个元素进行操作;而获取最终的value结果时,则将数组的元素求和。LongAdder的内部成员包含一个base值和一个cells数组。在最初无竞争时,只操作base的值;当线程执行CAS失败后, 才初始cells数组,并为线程分配所对应的元素。 相当于分段乐观锁!

//下面这种是传统做法
//定义一个原子对象
AtomicLong atomicLong = new AtomicLong(0);
atomicLong.incrementAndGet();
sout(atomicLong.get());
//下面这种是LongAdder做法
//定义一个LongAdder 对象
LongAdder longAdder = new LongAdder();
longAdder.add(1);
sout(longAdder.longValue());

AtomicLong使用内部变量value保存着实际的long值,所有的操作都是针对该value变量进行。也就是说,在高并发环境下, value变量其实是一个热点,也就是N个线程竞争一个热点。重试线程越多,就意味着CAS的失败概率越高,从而进入恶性CAS空自旋状态。 LongAdder的基本思路就是分散热点,将value值分散到一个数组中,不同线程会命中到数组的不同槽(元素)中,各个线程只对自己槽中的那个值进行CAS操作。这样热点就被分散了,冲突的概率就小很多。 使用LongAdder,即使线程数再多也不担心,各个线程会分配到多个元素上去更新,增加元素个数就可以降低 value的“热度”, AtomicLong中的恶性CAS空自旋就解决了。如果要获得完整的LongAdder存储的值,只要将各个槽中的变量值累加,返回最终的累加之后的值即可。LongAdder的实现思路与ConcurrentHashMap中分段锁基本原理非常相似,本质上都是不同的线程在不同的单元上进行操作,这样减少了线程竞争,提高了并发效率。

CAS优势与弊端:

CAS的优势主要有两点:

  • 属于无锁编程,线程不存在阻塞和唤醒这些重量级的操作。
  • 进程不存在用户态和内核态之间的运行切换,进程不需要承担频繁切换的开销。

CAS的弊端:

  • ABA问题。解决方法:版本号机制。JDK提供了AtomicStampedReference 版本号解决ABA问题。印戳 作为版本。
  • 只能保证一个共享变量之间的原子性操作 ,规避方法为:把多个共享变量合并成一个共享变量来操作。 规避方法合并成一个对象,JDK提供了AtomicReference类来保证引用对象之间的原子性 。
  • 无效CAS会带来开销问题,自旋CAS如果长时间不成功(不成功就一直循环执行,直到成功为止),就会给CPU带来非常大的执行开销。
提升CAS性能
  • 分散操作热点,使用LongAdder替代基础原子类AtomicLong, LongAdder将单个CAS热点(value值)分散到一个cells数组中。
  • 使用队列削峰,将发生CAS争用的线程加入一个队列中排队,降低CAS争用的激烈程度。自旋改成队列排队。

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

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

相关文章

idea 调试远程docker中的spring boot 项目

开发环境 idea-2023&#xff08;放心&#xff0c;旧版本也可以远程调试&#xff09; Java版本&#xff1a;17 生产环境 docker版本&#xff1a;23.0.3 Java版本1&#xff1a;openjdk:17.0.2&#xff08;基于Java17的项目&#xff09; Java版本2&#xff1a;adoptopenjdk:…

开源网安亮相粤港澳大湾区CIO高峰论坛,保障企业数字化安全转型

近日&#xff0c;由深圳市工业化与信息化局、深圳市科学技术协会指导&#xff0c;深圳市CIO协会主办的“2023中国(深圳)数字化转型大会暨粤港澳大湾区CIO高峰论坛”圆满完成。开源网安作为拥有软件安全领域全链条产品的厂商&#xff0c;携多年来打造的国产化软件安全替代方案&a…

EMC模式如何助力新能源服务商攻坚克难

01. 什么是合同能源管理&#xff1f; 合同能源管理(EMC-Energy Management Contract)是一种新型的市场化节能机制,其实质就是以减少的能源费用来支付节能项目全部成本的节能投资方式。&#xff1a;节能服务公司与用能单位以契约形式约定节能项目的节能目标&#xff0c;节能服务…

【Python脚本】视频稳像(Video Stabilization)

#【Python脚本】视频稳像(Video Stabilization) 参考&#xff1a;博客1 参考&#xff1a; 原文&#xff1a;https://blog.csdn.net/hjl240/article/details/52683738 开源&#xff1a;关键词 Video Stabilization 不错&#xff1a; https://github.com/yaochih/awesome-vide…

秒杀系统常见问题—如何避免库存超卖?

大家好&#xff01;我是sum墨&#xff0c;一个一线的底层码农&#xff0c;平时喜欢研究和思考一些技术相关的问题并整理成文&#xff0c;限于本人水平&#xff0c;如果文章和代码有表述不当之处&#xff0c;还请不吝赐教。 以下是正文&#xff01; 先看问题 首先上一串代码 …

Linux SUID提权脏牛提权

SUID提权 suid就是set user id 。设置了SUID后&#xff0c;文件启动的时候就会以root的权限去运行。就是一个普通用户运行的时候&#xff0c;因为有SUID&#xff0c;所以用root权限去运行它。 加SUID权限chmod ux 这里开始复现。 上传Linux提权信息检测脚本LinEnum find …

堆及其实现

目录 一&#xff1a;堆的概念及结构 1.概念 2.堆的性质 二&#xff1a;堆的实现 1.堆的构建 2.堆的销毁 3.数据的交换 4.堆的插入 5.堆的判空 6.堆的删除 7.取堆顶的数据 8.堆的数据个数 9.示例 三&#xff1a;完整的代码 一&#xff…

十、数据仓库详细介绍(数据质量)理论与经验

数据质量管理是对数据从计划、收集、记录、存储、回收、分析和展示生命周期的每个阶段里可能引发的数据质量问题&#xff0c;进行识别、度量、监控、预警等一系列管理活动&#xff0c;并通过改善和提高组织的管理水平使得数据质量获得进一步提高。数据质量管理的终极目标是通过…

会声会影2023最新完整版免费下载

会声会影2023操作简单&#xff0c;功能同样强大&#xff01;会声会影附带上百种特效、滤镜、转场、模板。同时各类专业级视频工具&#xff0c;如调色、遮罩、绿幕抠像、运动追踪、分屏创建器&#xff0c;满足更高标准的视频需求。这款软件上手操作简单易学&#xff0c;就算你在…

Linux之进程管理类命令

进程管理类命令 ps&#xff1a;查看当前系统进程状态 1&#xff09;基本语法 语法说明ps aux查看系统中所有进程ps -ef可以查看父子进程之间的关系 2&#xff09;选项说明 选项说明a列出带有终端的所有用户的进程x列出当前用户的所有进程&#xff0c;包括没有终端的进程u面…

C语言的一些杂记6

实现矩阵序号转置的三种方式 for (i 0; i < row * col; i)t[i / row][i % row] m[i % row][i / row];for (i 0; i < row; i)for (j 0; j < col; j)t[j][i] m[i][j];for (i 0; i < row; i)for (j 0; j < col; j)*(*(t j) i) *(*(m i) j); 变相数组 …

关于 arduino 中的 map(x, a, b,c,d)函数

函数名称&#xff1a;map() 包含形参&#xff1a; value&#xff1a;需要映射的值fromLow&#xff1a;输入值的最小值fromHigh&#xff1a;输入值的最大值toLow&#xff1a;输出值的最小值toHigh&#xff1a;输出值的最大值 功能&#xff1a;将一个值从一个范围映射到另一个…

【环境安装】Linux环境中docker安装redis

一、找到一个合适的docker的redis的版本 可以去docker hub中去找一下 https://link.juejin.cn/?targethttps%3A%2F%2Fhub.docker.com%2F_%2Fredis%3Ftab%3Dtags 二、使用docker安装redis 我这里安装了具体的某个版本 docker pull redis // 下载最新版Redis镜像 (等同于 : d…

UAS协议说明

1 概述 UAS(USB Attached SCSI)是一种位于SCSI协议框架下传输层的一种协议&#xff0c;其作用是通过基于USB的应用层协议约定&#xff0c;将SCSI的协议数据(Protocol Data Unit)用USB进行封装&#xff0c;从而实现使用USB物理连接进行SCSI协议通信的方式。 UAS实际上定义了两…

wireshark网络抓包详解

一、简介 Wireshark是一款非常流行的网络封包分析软件&#xff0c;可以截取各种网络数据包&#xff0c;并显示数据包详细信息。 为了安全考虑&#xff0c;wireshark只能查看封包&#xff0c;而不能修改封包的内容&#xff0c;或者发送封包。 wireshark能获取HTTP&#xff0c;也…

【Android】(最新)跑马灯文字水平滚动(79/100)

先上效果&#xff1a; Android系统中TextView实现跑马灯效果&#xff0c;必须具备以下几个条件&#xff1a; android:singleLine“true”android:ellipsize“marquee”android:marqueeRepeatLimit“marquee_forever”TextView必须单行显示&#xff0c;即内容必须超出TextView…

Jetpack Compose 实现了一个丝滑流畅的页面展开和关闭的效果动画

Jetpack Compose 将动画实现的门槛降低了&#xff0c;不过Compose目前还不支持共享元素过渡。 (上篇文章Jetpack Compose开发的本地笔记本)的动画效果的实现 转跳前的准备工作 定义State枚举类来表示页面的三种状态: Closing(关闭状态) Closed(关闭完成状态) Opening(展开状…

找不到vcruntime140.dll,无法继续执行代码?多种解决方法解析

找不到vcruntime140.dll,无法继续执行代码&#xff1f;当你在尝试运行某个程序时&#xff0c;突然弹出一条错误提示框&#xff0c;告诉你无法继续执行代码&#xff0c;因为找不到vcruntime140.dll。这个问题很常见&#xff0c;但是它可能会让你感到困惑和疑惑。这篇文章将详细介…

chatgpt赋能Python-python_numpy遍历

Python NumPy遍历&#xff1a;使用高效的方式为数据科学家节省时间和精力 Python语言在数据科学领域中的地位越来越重要&#xff0c;并成为了数据科学家的首选语言之一。在解决数据问题时&#xff0c;NumPy模块是Python程序员经常使用的一个重要库。NumPy提供了快速的数组操作…

【大数据实训】—Hadoop开发环境搭建(一)

【大数据实训】—Hadoop开发环境搭建&#xff08;一&#xff09; 第一关、任务描述 本关任务&#xff1a;配置JavaJDK。 相关知识 配置开发环境是我们学习一门IT技术的第一步&#xff0c;Hadoop是基于Java开发的&#xff0c;所以我们学习Hadoop之前需要在Linux系统中配置Jav…