Java并发的CAS原理详解[通俗易懂]

Java并发的CAS原理详解[通俗易懂]Java并发编程中的CAS原理是很重要的概念。CAS加volatile关键字是实现并发包的基石。没有CAS就不会有并发包,synchronized是一种独占锁、悲观锁,java.util.concurrent中借助了CAS指令实现了一种区别于synchronized的一种乐观锁。乐观锁和悲观锁的概念请参考Java中的21种锁。在Java中java.util.concurrent.atomic包下面的原子变量就是使用了乐观锁的一种实现方式CAS实现。在JDK5之前Java语言是靠synchroniz

大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。

Jetbrains全系列IDE稳定放心使用

Java重要知识点学习整理笔记。

序号 文章
1 Java并发的CAS原理详解
2 Java并发的ABA原理详解
3 Java的18种Queue
4 一篇文章整理Java的volatile
5 Java集合的线程不安全
6 Java中的21种锁
7 JVM进阶之思维导图
8 Java的HashMap原理总结(问答式学习)

Java并发编程中的CAS原理是很重要的概念。

CAS加volatile关键字是实现并发包的基石。没有CAS就不会有并发包,synchronized是一种独占锁、悲观锁,java.util.concurrent中借助了CAS指令实现了一种区别于synchronized的一种乐观锁。

乐观锁和悲观锁的概念请参考Java中的21种锁

在Java中java.util.concurrent.atomic包下面的原子变量就是使用了乐观锁的一种实现方式CAS实现。

在JDK 5之前Java语言是靠synchronized关键字保证同步的,这会导致有锁。

锁机制存在以下问题:

  1. 在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
  2. 一个线程持有锁会导致其它所有需要此锁的线程挂起。
  3. 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。

volatile是不错的机制,但是volatile不能保证原子性。因此对于同步最终还是要回到锁机制上来。

乐观锁用到的机制就是CAS,Compare and Swap。

一、案例引入CAS

案例:甲看见一个三角形积木,觉得不好看,想替换成五边形,但是乙想把积木替换成四边形。(前提条件,只能被替换一次)
在这里插入图片描述
甲比较鸡贼,想到了一个办法:“我把积木带到另外一个房间里面去替换,并上锁,就不会被别人打扰了。”(这里用到了排他锁synchronized

乙觉得甲太不厚道:“房间上了锁,我进不去,我也看不见积木长啥样。(因上了锁,所以不能访问)”

在这里插入图片描述
于是甲、乙想到了另外一个办法:谁先抢到积木,谁先替换,如果积木形状变了,则不允许其他人再次替换。(比较并替换CAS

于是他们就开始抢三角形积木:

  • 场景1甲抢到,替换成五边形,乙不能替换

    • 假如甲先抢到了,积木还是三角形的,就把三角形替换成五边形了。
      在这里插入图片描述
    • 乙后抢到,积木已经变为五边形了,乙就没机会替换了(因为甲、乙共一次替换机会)。
      在这里插入图片描述
  • 场景2乙抢到未替换,甲替换成功

    • 假如乙先抢到了,但是突然觉得三角形也挺好看的,没有替换,放下积木就走开了。

    • 然后甲抢到了积木,积木还是三角形的,想到乙没有替换,就把三角形替换成五边形了。
      在这里插入图片描述

  • 场景3乙抢到,替换成三角形,甲替换成五边形,ABA问题

    • 假如乙先抢到了,但是觉得这个三角形是旧的,就换了另外一个一摸一样的三角形,只是积木比较新。
    • 然后甲抢到了积木,积木还是三角形的,想到乙没有替换,就把三角形替换成五边形了。
      在这里插入图片描述

二、Java CAS介绍

CAS的全称Compare-And-Swap(比较并交换)。比较变量的现在值与之前的值是否一致,若一致则替换,否则不替换。

CAS的作用原子性更新变量值,保证线程安全。

CAS指令:需要有三个操作数,变量的当前值(V)旧的预期值(A)准备设置的新值(B)

CAS指令执行条件:当且仅当V=A时,处理器才会设置V=B,否则不执行更新。

CAS的返回指:V的之前值。

CAS处理过程:原子操作,执行期间不会被其他线程中断,线程安全。

CAS并发原语:体现在Java语言中sun.misc.Unsafe类的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令,这是一种完全依赖于硬件的功能,通过它实现了原子操作。由于CAS是一种系统原语,原语属于操作系统用于范畴,是由若干条指令组成,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,所以CAS是一条CPU的原子指令,不会造成所谓的数据不一致的问题,所以CAS是线程安全的。

三、写几行代码讲一下CAS

volatile时,讲到了如何使用原子整型类AtomicInteger来解决volatile的非原子性问题,保证多个线程执行num++的操作,最终执行的结果与单线程一致,输出结果为20000。

这次还是用AtomicInteger

  • 首先定义atomicInteger变量的初始值等于10,主内存中的值设置为10
AtomicInteger atomicInteger = new AtomicInteger(10);
  • 然后调用atomicInteger的CAS方法,先比较当前变量atomicInteger的值是否是10,如果是,则将变量的值设置为20
atomicInteger.compareAndSet(10, 20);

设置成功,atomicInteger更新为20

  • 当我们再次调用atomicInteger的CAS方法,先比较当前变量atomicInteger的值是否是10,如果是,则将变量的值设置为30
atomicInteger.compareAndSet(10, 30);

设置失败,因atomicInteger的当前值为20,而比较值是10,所以比较后,不相等,故不能进行更新。

package com.jackson0714.passjava.threads;
import java.util.concurrent.atomic.AtomicInteger;
/** 演示CAS compareAndSet 比较并交换 */
public class CASDemo { 
   
    public static void  main(String[] args) { 
   
        AtomicInteger atomicInteger = new AtomicInteger(10);
        Boolean result1 = atomicInteger.compareAndSet(10,20);
        System.out.printf("当前atomicInteger变量的值:%d 比较结果%s\r\n", atomicInteger.get(), result1);
        Boolean result2 = atomicInteger.compareAndSet(10,30);
        System.out.printf("当前atomicInteger变量的值:%d, 比较结果%s\n" , atomicInteger.get(), result2);
    }
}
当前atomicInteger变量的值:20 比较结果true
当前atomicInteger变量的值:20, 比较结果false

来对比看下原理图理解下上面代码的过程

  • 第一步:线程1和线程2都有主内存中变量的拷贝,值都等于10
    在这里插入图片描述

  • 第二步:线程1想要将值更新为20,先要将工作内存中的变量值与主内存中的变量进行比较,值都等于10,所以可以将主内存中的值替换成20
    在这里插入图片描述

  • 第三步:线程1将主内存中的值替换成20,并将线程1中的工作内存中的副本更新为20
    在这里插入图片描述

  • 第四步:线程2想要将变量更新为30,先要将线程2的工作内存中的值与主内存进行比较10不等于20,所以不能更新
    在这里插入图片描述

  • 第五步:线程2将工作内存的副本更新为与主内存一致:20
    在这里插入图片描述

上述的场景和我们用Git代码管理工具是一样的,如果有人先提交了代码到develop分支,另外一个人想要改这个地方的代码,就得先pull develop分支,以免提交时提示冲突。

四、讲下CAS底层原理

源码调试

这里用atomicIntegergetAndIncrement()方法来讲解,这个方法里面涉及到了比较并替换的原理。

示例如下:

public static void  main(String[] args) throws InterruptedException { 
   
    AtomicInteger atomicInteger = new AtomicInteger(10);
    Thread.sleep(100);

    new Thread(() -> { 
   
        atomicInteger.getAndIncrement();
    }, "aaa").start();

    atomicInteger.getAndIncrement();
}
  1. 首先需要开启IDEA的多线程调试模式
  2. 我们先打断点到17行,main线程执行到此行,子线程aaa还未执行自增操作
    在这里插入图片描述
    getAndIncrement方法会调用unsafe的getAndAddInt方法,
public final int getAndIncrement() { 
   
    return unsafe.getAndAddInt(this, valueOffset, 1);
}
  1. 在源码getAndAddInt方法的361行打上断点,main线程先执行到361行
public final int getAndAddInt(Object var1, long var2, int var4) { 
   
    int var5;
    do { 
   
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    return var5;
}

源码解释: 重点!!!

  • var1:当前对象,我们定义的atomicInteger
  • var2:当前对象的内存偏移量
  • var4:当前自增多少,默认为1,且不可设为其他值
  • var5:当前变量的值
  • this.getIntVolatile(var1, var2):根据当前对象var1和对象的内存偏移量var2得到主内存中变量的值,赋值给var5,并在main线程的工作内存中存放一份var5的副本
    在这里插入图片描述
  1. 在362行打上断点,main线程继续执行一步

    var5获取到主内存中的值为10
    在这里插入图片描述

  2. 切换到子线程aaa,还是在361行断点处,还未获取主内存的值
    在这里插入图片描述

  3. 子线程aaa继续执行一步,获取到var5的值等于10
    在这里插入图片描述

  4. 切换到main线程,进行比较并替换

    this.compareAndSwapInt(var1, var2, var5, var5 + var4)’

    var5=10,通过var1和var2获取到的值也是10,因为没有其他线程修改变量。compareAndSwapInt的源码后面再说。

    所以比较后,发现变量没被其他线程修改,可以进行替换,替换值为var5+var4=11,变量值替换后为 11,也就是自增1。这行代码执行结果返回true(自增成功),退出do while循环。return值为变量更新前的值10。
    在这里插入图片描述

  5. 切换到子线程aaa,进行比较并自增

    因为此时aaa线程的var5=10,而主内存中的值已经更新为11了,所以比较后发现被其他线程修改了,不能进行替换,返回false,继续执行do while循环。
    在这里插入图片描述

  6. 子线程aaa继续执行,重新获取到的var=11
    在这里插入图片描述

  7. 子线程aaa继续执行,进行比较和替换,结果为true

    因var5=11,主内存中的变量值也等于11,所以比较后相等,可以进行替换,替换值为var5+var4,结果为12,也就是自增1。退出循环,返回变量更新前的值var5=11。
    在这里插入图片描述

至此,getAndIncrement方法的整个原子自增的逻辑就debug完了。所以可以得出结论:

先比较线程中的副本是否与主内存相等,相等则可以进行自增,并返回副本的值,若其他线程修改了主内存中的值,当前线程不能进行自增,需要重新获取主内存的值,然后再次判断是否与主内存中的值是否相等,以此往复。

五、CAS有什么问题?

aaa线程可能会出现循环多次的问题,因为其他线程可能将主内存的值又改了,但是aaa线程拿到的还是老的数据,就会出现再循环一次,就会给CPU带来性能开销。这个就是自旋

  1. 频繁出现自旋,循环时间长,开销大(因为执行的是do while,如果比较不成功一直在循环,最差的情况,就是某个线程一直取到的值和预期值都不一样,这样就会无限循环)

  2. 只能保证一个共享变量的原子操作

    • 当对一个共享变量执行操作时,我们可以通过循环CAS的方式来保证原子操作
    • 但是对于多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候只能用来保证原子性
  3. 引出来ABA问题(第1步中读取的值是A,并且在第3步修改成功了,我们就能说它的值在第1步和第3步之间没有被其他线程改变过了吗?)

参考:

Java中CAS详解

CAS原理分析

我跟老婆讲CAS原理-被鄙视太简单-15张图-源码调试

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/183693.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)
blank

相关推荐

  • python自动化运维面试题_运维面试题(含答案)「建议收藏」

    python自动化运维面试题_运维面试题(含答案)「建议收藏」运维工程师面试题姓名:答题时间:1.新安装MYSQL后怎样提升MYSQL的安全级别?A.修改mysql默认端口B.linux下可以通过iptables来限制访问mysql端口的IP地址C.对所有用户设置较复杂密码并严格指定对应账号的访问IP(可在mysql库中user表中指定用户的访问可访问IP地址)D.root特权账号的处理(建议给root账号设置强密码,并指定只允许本地登录)E.开启二进制查询…

  • 电容分类_电解电容和薄膜电容的区别

    电容分类_电解电容和薄膜电容的区别一、按照功能  1.名称:聚酯(涤纶)电容   符号:(CL)  电容量:40p–4μ  额定电压:63–630V  主要特点:小体积,大容量,耐热耐湿,稳定性差  应用:对稳定性和损耗要求不高的低频电路  2.名称:聚苯乙烯电容  符号:(CB)  电容量:10p–1μ  额定电压:100V–30KV

  • mysql 8.0 忘记root密码_linux系统重置root密码

    mysql 8.0 忘记root密码_linux系统重置root密码在安装完数据库后,由于自己不小心直接关闭了安装窗口,或者长时间没有使用root用户登录系统,导致忘记了root密码,这时就需要重置MySQL的root密码。当然,最简单方式自然是删除数据库的data目录,然后重新安装数据库。但是很多时间我们需要保留data目录中的数据,所以就需要查找如何重置root密码。我们知道,在知道root密码时,可以使用“ALTERUSER‘root’@’local…

  • Webstorm的一些常用快捷键

    Webstorm的一些常用快捷键下面是Webstorm的一些常用快捷键:1.ctrl+shift+n:打开工程中的文件,目的是打开当前工程下任意目录的文件。2.ctrl+j:输出模板3.ctrl+b:跳到变量申明处4.ctrl+alt+T:围绕包裹代码(包括zencoding的WrapwithAbbreviation)5.ctrl+[]:匹配{}[]6

  • 企业如何制定SOP?

    企业如何制定SOP?企业制定SOP的根本目的是为了提高企业的管理运营能力,使得企业运行效率和运行效果得以改善,从而为企业带来更大的效益,每个企业由于管理模式不同,制定SOP会有所区别。

  • 一步一步学android OpenGL ES2.0编程(3)

    一步一步学android OpenGL ES2.0编程(3)

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号