Java-CAS 原理与 JUC 原子类

news2024/11/16 20:41:51

由于 JVM 的 synchronized 重量级锁涉及到操作系统(如 Linux) 内核态下的互斥锁(Mutex)的使用, 其线程阻塞和唤醒都涉及到进程在用户态和到内核态频繁切换, 导致重量级锁开销大、性能低。 而 JVM 的 synchronized 轻量级锁使用 CAS(Compare and Swap) 进行自旋抢锁, CAS 是CPU 指令级的原子操作, 并处于用户态下, 所以 JVM 轻量级锁开销较小。

什么是 CAS

CAS 的英文全称为 Compare and Swap,翻译成中文为“比较并交换” 。 JDK5 所增加的 JUC(java.util.concurrent)并发包, 对操作系统的底层 CAS 原子操作进行了封装,为上层 Java 程序提供了 CAS 操作的 API。

Unsafe 类中的 CAS 方法

Unsafe 是位于 sun.misc 包下的一个类,主要提供一些用于执行低级别、不安全的底层操作,如直接访问系统内存资源、自主管理内存资源等, Unsafe 大量的方法都是 native 方法,基于 C++语言实现, 这些方法在提升 Java 运行效率、增强 Java 语言底层资源操作能力方面起到了很大的作用。
Unsafe 类的全限定名为 sun.misc.Unsafe,从名字中我们可以看出来这个类对普通程序员来说是“危险”的,一般的应用开发都不会涉及到此类, Java 官方也不建议直接在应用程序中使用。

操作系统层面的 CAS 是一条 CPU 的原子指令(cmpxchg 指令),正是由于该指令具备了原子性,所以使用 CAS 操作数据时不会造成数据不一致问题, Unsafe 提供的 CAS 方法,直接通过native 方式(封装 C++代码)调用了底层的 CPU 指令 cmpxchg。

完成 Java 应用层的 CAS 操作,主要涉及到的 Unsafe 方法调用,具体如下:

(1) 获取 Unsafe 实例。
(2) 调用 Unsafe 提供的 CAS 方法, 这些方法主要封装了底层 CPU 的 CAS 原子操作。
(3)调用 Unsafe 提供的字段偏移量方法, 这些方法用于获取对象中的字段(属性)偏移量,

此偏移量值需要作为参数提供给 CAS 操作。

获取 Unsafe 实例

Unsafe 类是一个“final”修饰的不允许继承的最终类,而且其构造函数是 private 类型的方法,具体的源码如下:

public final class Unsafe {
  private static final Unsafe theUnsafe;
  public static final int INVALID_FIELD_OFFSET = -1;
  private static native void registerNatives();
  // 构造函数是 private 的,不允许外部实例化
  private Unsafe() {
  }
  ...
}
调用 Unsafe 提供的 CAS 方法

Unsafe 提供的 CAS 方法,主要如下:

/**
* 定义在 Unsafe 类中的三个 “比较并交换”原子方法
* @param o 需要操作的字段所处的对象
* @param offset 需要操作的字段的偏移量(相对的,相对于对象头)
* @param expected 期望值(旧的值)
* @param update 更新值(新的值)
* @return true 更新成功 | false 更新失败
*/
public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object update);

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

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

Unsafe 提供的 CAS 方法包含四个操作数——字段所处的对象、字段内存位置、预期原值及新值。 在执行 Unsafe 的 CAS 方法的时候, 这些方法首先将内存位置的值与预期值(旧的值) 比较,如果相匹配,那么处理器会自动将该内存位置的值更新为新值, 并返回 true; 如果不相匹配,处理器不做任何操作,并返回 false。

Unsafe 的 CAS 操作会将第一个参数(对象的指针、地址)与第二个参数(字段偏移量)组合在一起,计算出最终的内存操作地址。

调用 Unsafe 提供的偏移量相关

Unsafe 提供的获取字段(属性) 偏移量的相关操作,主要如下:

/**
* 定义在 Unsafe 类中的几个 获取字段偏移量的方法
* @param o 需要操作字段的反射
* @return 字段的偏移量
*/
public native long staticFieldOffset(Field field);

public native long objectFieldOffset(Field field);

staticFieldOffset 方法用于获取静态属性 Field 在 Class 对象中的偏移量, 在 CAS 操作静态属性时,会用到这个偏移量。 objectFieldOffset 方法用于获取非静态 Field(非静态属性) 在 Object 实例中的偏移量, 在 CAS 操作对象的非静态属性时, 会用到这个偏移量。

一个获取非静态 Field(非静态属性)在 Object 实例中的偏移量的示例,代码如下:

public static class Obj {

  private static final Unsafe unsafe = getUnsafe();

  private static long stateOffset;

  private volatile int state;

  public static Unsafe getUnsafe() {
      try {
          Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
          theUnsafe.setAccessible(true);
          return (Unsafe) theUnsafe.get(null);
      } catch (Exception e) {
          e.printStackTrace();
      }
      return null;
  }

  static {
      try {
          stateOffset = unsafe.objectFieldOffset(Obj.class.getDeclaredField("state"));
      } catch (Exception ex) {
          throw new Error(ex);
      }
  }

  public int getState() {
      return state;
  }

  public void setState(int state) {
      this.state = state;
  }

  public final boolean compareAndSet(int expect, int update) {
      return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
  }
  
  public static void main(String[] args){
    Obj obj = new Obj();
    boolean b = obj.compareAndSet(0, 10);
    System.out.println("b:"+b+" , state:"+obj.getState());
  }
}
使用 CAS 进行“无锁编程”

CAS 是一种无锁算法,该算法关键依赖两个值——期望值(就值)和新值,底层 CPU 利用原子操作,判断内存原值与期望值是否相等, 如果相等则给内存地址赋新值,否则不做任何操作。

使用 CAS 进行“无锁编程” (Lock Free) 的步骤大致如下:

(1)获得字段的期望值(oldValue) 。
(2) 计算出需要替换的新值(newValue) 。
(3) 通过 CAS 将新值(newValue)放在字段的内存地址上,如果 CAS 失败则重复第 1 步到第 2 步,一直到 CAS 成功, 这种重复俗称 CAS 自旋。

使用 CAS 进行“无锁编程”的伪代码,大致如下:

do{
   获取字段的期望值(oldValue)
   计算需要替换的新值(newValue)
}while(!CAS(内存地址, oldValue, newValue))

下面用一个简单的例子,对以上伪代码进行举例说明。

假如某个内存地址(某对象的属性)的值为 100,现在有两个线程(线程 A、 线程 B)使用CAS 无锁编程对该内存地址进行更新,线程 A 欲将其值更新为 200,线程 B 欲将其值更新为 300,具体如图 3-1 所示。

截图

由于线程是并发执行,谁都有可能先执行。 但是 CAS 是原子操作,对同一个内存地址的 CAS操作在同一时刻只能执行一个。所以在这个例子中,要么线程 A 先执行,要么线程 B 先执行。假设线程 A 的 CAS(100,200)执行在前,由于内存地址的旧值 100 与该 CAS 的期望值 100 相等,所以线程 A 会操作成功,内存地址的值被更新为 200。

线程 A 执行成功 CAS(100,200) 之后,内存地址的值具体如图 3-2 所示。

截图

接下来执行线程 B 的 CAS(100,300)操作,此时内存地址的值为 200,不等于 CAS 的期望值 100,线程 B 操作失败。线程 B 只能自旋,开始新的循环,这一轮循环首先获取到内存地址的值 200,然后进行 CAS(200,300)操作,这一次内存地址的值与 CAS 的预期值(oldValue)相等,线程 B 操作成功。
当 CAS 进行内存地址的值与预期值比较时,如果相等,则证明内存地址的值没有被修改, 可以替换成新值,然后继续往下运行;如果不相等,说明明内存地址的值已经被修改,放弃替换操作,然后重新自旋。当并发修改的线程少, 冲突出现的机会少时, 自旋的次数也会很少, CAS 性能会很高;当并发修改的线程多,冲突出现的机会高时,自旋的次数也会很多, CAS 性能会大大降低。 所以,提升 CAS 无锁编程的效率,关键在于减少冲突的机会。

JUC原子类

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

JUC 中的 Atomic 原子操作包

​ Atomic 操作翻译成中文, 是指一个不可中断的操作,即使在多个线程一起执行 Atomic 类型操作的时候,一个操作一旦开始,就不会被其他线程中断。所谓Atomic 类,指的是具有原子操作特征的类。

JUC 并发包中原子类的位置

JUC并发包中原子类,都存放在java.util.concurrent.atomic 类路径下,具体如图

image-20210627162356019

根据操作的目标数据类型,可以将JUC包中的原子类分为4类:

  1. 基本原子类
  2. 数组原子类
  3. 原子引用类型
  4. 字段更新原子类

基本原子类

基本原子类的功能,是通过原子方式更新Java基础类型变量的值。 基本原子类主要包括了一下三个:

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

数组原子类

数组原子类的功能,是通过原子方式更新数组里的某个元素的值。 数组原子类主要包括一下三个:

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

引用原子类

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

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

AtomicMarkableReference类将boolean 标记与引用关联起来,可以解决使用AtomicBoolean 进行原子更新时可能出现的ABA问题

AtomicStampedReference 类将整数值与引用关联起来, 可以解决使用AtomicInteger 进行原子更新时出现的ABA问题

字段更新原子类型

字段更新原子类

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

  • AtomicIntegerFieldUpdater : 原子更新整型字段的更新器
  • AtomicLongFieldUpdater : 原子更新长整型字段的更新器
  • AtomicReferenceFieldUpdater : 原子更新引用类型里的字段

##### AtomicInteger 线程安全原理

基础原子类(以 AtomicInteger 为例 )主要通过 CAS 自旋 + volatile 相结合的方案实现,既保障了变量操作的线程安全性,又避免了 synchronized 重量级锁的高开销, 使得 Java 程序的执行效率大为提升。

注:CAS 用于保障变量操作的原子性, volatile 关键字用于保障变量的可见性,二者常常结合使用。

下面以 AtomicInteger 源码为例,分析一下原子类的 CAS 自旋 + volatile 相结合的实现方案。AtomicInteger 源码的具体的代码如下:

public class AtomicInteger extends Number implements java.io.Serializable {
    //Unsafe 类实例
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    //内部 value 值,使用 volatile 保证线程可见性
    private volatile int value;
    //value 属性值的地址偏移量
    private static final long valueOffset;
    static {
        try {
            //计算 value 属性值的地址偏移量
            valueOffset = unsafe.objectFieldOffset(
            AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
    //初始化
    public AtomicInteger(int initialValue) {
    	value = initialValue;
    }
    //获取当前 value 值
    public final int get() {
    	return value;
    }
    //方法:返回旧值并赋新值
    public final int getAndSet(int newValue) {
        for (;;) {//自旋
            int current = get();//获取旧值
            //以 CAS 方式赋值,直到成功返回
            if (compareAndSet(current, newValue)) 
                return current;
		}
	}
    //方法: 封装底层的 CAS 操作, 对比 expect(期望值)与 value,不同返回 false
    //expect 与 value 相同, 则将新值赋给 value, 并返回 true
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
    //方法: 安全自增 i++
    public final int getAndIncrement() {
        for (;;) { //自旋
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next))
            	return current;
        }
    }
    //方法:自定义增量数
    public final int getAndAdd(int delta) {
        for (;;) { //自旋
            int current = get();
            int next = current + delta;
            if (compareAndSet(current, next))
            	return current;
        }
    }
    //方法:类似++I,返回自增后的值
    public final int incrementAndGet() {
        for (;;) { //自旋
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next))
            	return next;
        }
    }
    //方法:返回加上 delta 后的值
    public final int addAndGet(int delta) {
        for (;;) { //自旋
            int current = get();
            int next = current + delta;
            if (compareAndSet(current, next))
            	return next;
        }
    }
    //...省略其他源码
}
AtomicInteger 源码中的主要方法,都是通过 CAS 自旋实现的。 CAS 自旋的主要操作为: 如果一次 CAS 操作失败,则获取最新的 value 值后,再次进行 CAS 操作,直到成功。
另外, AtomicInteger 所包装的内部 value 成员, 是一个使用关键字 volatile 修饰的内部成员。关键字 volatile 的原理比较复杂,简单的说,该关键字可以保证任何线程在任何时刻总能拿到该变量的最新值,其目的在于保障变量值的线程可见性。  
对象操作的原子性

​ 基础的原子类型只能保证一个变量的原子操作,当需要对多个变量进行操作时, CAS 无法保证原子性操作,这时可以用 AtomicReference(原子引用类型)保证对象引用的原子性。
​ 简单来说,如果需要同时保障对多个变量操作的原子性,就可以把多个变量放在一个对象里进行操作。
​ 与对象操作的原子性有关的原子类型,除了引用类型原子类之外,还包括属性更新原子类。

引用类型原子类

引用类型原子类包括以下3种:

  • AtomicReference: 基础的引用原子类
  • AtomicMarkableReference : 带修改标志的引用原子类
  • AtomicStampedReference : 带印戳的引用原子类

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

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

相关文章

影响汇率的因素?fpmarkets澳福总结几个

汇率对于刚刚开始外汇交易的新手来说非常重要,这不是没有道理的,了解汇率如何变化以及怎么变化有助于在外汇交易中获得稳定的利润。那么影响汇率的因素有哪些?fpmarkets澳福总结几个。 任何国家货币的汇率都是由市场决定的。主要的市场因素是…

汽车网络基础知识 要点

在以太网开发中,常常会听到一些专业名词,例如PHY,MAC,MII,switch,下面是解释 PHY PHY 是物理接口收发器,它实现物理层。包括 MII/GMII (介质独立接口) 子层、PCS (物理编码子层) 、PMA (物理介…

高颜值HMI触控界面一出,价值感飙升,瞬间感觉消费不起了。

千万不要觉得用户很理性,其实用户都是“好色之徒”,判断产品价值基本上靠眼睛,颜值高的价格高,质量高,反之质量低,价格低。如果通过精心的高颜值设计,能让你的产品价值感拉满,你不心…

html密码访问单页自定义跳转页面源码

内容目录 一、详细介绍二、效果展示1.部分代码2.效果图展示 三、学习资料下载 一、详细介绍 密码访问单页自定义跳转页面,修改了的密码访问单页,添加了js自定义密码跳转页面。需要正确输入密码才能跳转目标网址。 二、效果展示 1.部分代码 代码如下&…

等保测评的知识

结合自己所学的知识和网络上的一些知识做个小总结。 目录 一、概念: 二、等级划分: 三、技术要求: 四、管理要求: 五、等保测评实施过程: 六、典型的网络架构: 一、概念: 全称为信息安全等级保…

Vue2(五):收集表单数据、过滤器、自定义指令、Vue的生命周期

一、收集表单数据 爱好&#xff1a;学习<input type"checkbox" value"study" v-model"hobby">打游戏<input type"checkbox" value"games" v-model"hobby">吃饭<input type"checkbox" v…

创建局域网分享图片及html页面服务(简单讲下)

目录 1. 使用Python的SimpleHTTPServer&#xff08;适用于Windows&#xff09; 打开其中的.html文件&#xff1a; 打开其中的.png文件&#xff1a; 推荐第2种&#xff1a; 2. 使用Node.js和http-server&#xff08;适用于所有平台&#xff09; 安装http-server&#xff08;…

PSCA复位控制集成之复位信号

组件可能支持两种基本的复位类型。 • 冷复位&#xff1a;重置组件中的所有逻辑。用作上电复位。 • 热复位&#xff1a;重置组件中的大部分逻辑。通常&#xff0c;复位的范围是所有功能逻辑。不包括在热复位中的逻辑会随组件类型而变化&#xff0c;但通常会排除诸如调试和 R…

【论文阅读】Scalable Diffusion Models with Transformers

DiT&#xff1a;基于transformer架构的扩散模型。 paper&#xff1a;[2212.09748] Scalable Diffusion Models with Transformers (arxiv.org) code&#xff1a;facebookresearch/DiT: Official PyTorch Implementation of "Scalable Diffusion Models with Transformer…

13 秒插入 30 万条数据,这才是 Java 批量插入正确的姿势!

本文主要讲述通过MyBatis、JDBC等做大数据量数据插入的案例和结果。 30万条数据插入插入数据库验证 实体类、mapper和配置文件定义 User实体 mapper接口 mapper.xml文件 jdbc.properties sqlMapConfig.xml 不分批次直接梭哈 循环逐条插入 MyBatis实现插入30万条数据 J…

python 深度学习 记录遇到的报错问题12

本篇继python 深度学习 记录遇到的报错问题11_undefined symbol: __nvjitlinkadddata_12_1, version-CSDN博客 目录 一、AttributeError: module ‘tensorflow‘ has no attribute ‘app‘ 二、AttributeError: module tensorflow has no attribute placeholder 三、Attribu…

基于51单片机火灾报警器设计

一、系统方案 1、本设计采用51单片机作为主控器。 2、液晶1602显示。 3、采集温度值&#xff0c;烟雾值。 4、按键设置温度、烟雾报警值&#xff0c;测量值超过设置值蜂鸣器报警。 5、按键布防&#xff0c;有人闯入&#xff0c;声光报警。 二、硬件设计 原理图如下&#xff1a…

牛客DP34 前缀和

解题思路 题目解析如图 思路 算出每个位置的到第一个位置的总和 比如 第一个位置 1 总和 1 第二个位置 2 总和 3 第三个位置 4 总和 7 要算 2到3 位置的前缀和 用3位置的总和减去1位置的总和即可 还要处理一个边界情况 如果1到1位置的前缀和那么就是 …

【React】Vite创建React+TS项目

前提条件 有node环境&#xff0c;且node版本>18.0.0 创建项目 npm create vitelatest1.起项目名 2.选择框架 3.选择语言 TypeScript SWC 是指 Vite 使用 SWC&#xff08;Speedy Web Compiler&#xff09;作为 TypeScript 的编译器。 SWC 是一个针对 JavaScript 和 Ty…

HarmonyOS NEXT应用开发之SideBarContainer侧边栏淡入淡出动效实现案例

介绍 在2in1或平板上&#xff0c;群聊侧边栏是一种较为常用的功能&#xff0c;虽然HarmonyOS已经具备了基本的动效&#xff0c;但是部分情况下开发者可能有定制侧边栏动效的需求&#xff0c;本例主要介绍了如何基于显式动画实现侧边栏的淡入淡出动效。 效果图预览 使用说明&a…

力扣模板题:检测字符串中数字是否递增

bool areNumbersAscending(char * s){//双指针操作&#xff0c;前指针保存前面一个数字字符int p0,q0;for(int i0;s[i];i){if(s[i]>0&&s[i]<9){pp*10s[i]-0;if(s[i1] ||s[i1]\0){//进行比较, 比较过后将p赋值q&#xff0c;q记录前面一个数字,因为数字均为小于100…

Git 仓库瘦身与 LFS 大文件存储

熟悉 Git 的小伙伴应该都知道随着 Git 仓库维护的时间越来越久&#xff0c;追踪的文件越来越多&#xff0c;git 存储的 objects 数量会极其庞大&#xff0c;每次从远程仓库 git clone 的时候都会墨迹很久。如果我们不小心 git add 了一个体积很大的文件&#xff0c;且 git push…

通俗易懂的精度Precision和召回率Recall解释,看这篇就行,5分钟记住。

一、背景 因为我是做机器人方向的&#xff0c;不可避免的涉及到视觉方向的内容&#xff0c;还有审稿的时候也会看到识别相关的内容&#xff0c;其中衡量识别效果的指标包括精度Precision和召回率Recall&#xff0c;虽然很好理解&#xff0c;但每次都记不住&#xff0c;趁这次机…

【递归专题】【蓝桥杯备考训练】:有序分数、正则问题、带分数、约数之和、分形之城【已更新完成】

目录 1、有序分数&#xff08;usaco training 2.1&#xff09; 2、正则问题&#xff08;第八届蓝桥杯省赛C A组 & Java A组&#xff09; 3、带分数&#xff08;第四届蓝桥杯省赛Java A组/B组 & C B组/C组&#xff09; 4、约数之和&#xff08;《算法竞赛进阶指南》…

Flink实时数仓之用户埋点系统(二)

用户埋点平台-数仓建模 1、数据仓库 1.1 数据仓库的构建 1.1.1 数据模型 数据模型就是数据组织和存储方法&#xff0c;它强调从业务、数据存取和使用角度合理存储数据。只有将数据有序的组织和存储起来之后&#xff0c;数据才能得到高性能、低成本、高效率、高质量的使用。…