Java-CAS 原理与 JUC 原子类

由于 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 : 带印戳的引用原子类

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

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

相关文章

免费阅读篇 | 芒果YOLOv8改进114:上采样Dysample:顶会ICCV2023,轻量级图像增采样器,通过学习采样来学习上采样,计算资源需求小

💡🚀🚀🚀本博客 改进源代码改进 适用于 YOLOv8 按步骤操作运行改进后的代码即可 该专栏完整目录链接: 芒果YOLOv8深度改进教程 🚀🚀🚀 DySample是一个超轻量级和有效的动态上采样器…

DDos攻击如何被高防服务器有效防范?

德迅云安全-领先云安全服务与解决方案提供商 什么是DDos攻击? DDos攻击是一种网络攻击手段,旨在通过使目标系统的服务不可用或中断,导致无法正常使用网络服务。DDos攻击可以采取多种方式实施,包括洪水攻击、压力测试、UDP Flood…

HTML静态网页成品作业(HTML+CSS)——游戏战地介绍设计制作(4个页面)

🎉不定期分享源码,关注不丢失哦 文章目录 一、作品介绍二、作品演示三、代码目录四、网站代码HTML部分代码 五、源码获取 一、作品介绍 🏷️本套采用HTMLCSS,未使用Javacsript代码,共有4个页面。 二、作品演示 三、代…

关于PXIE3U18槽背板原理拓扑关系

如今IT行业日新月异,飞速发展,随之带来的是数据吞吐量的急剧升高。大数据,大存储将成为未来数据通信的主流,建立快速、大容量的数据传输通道将成为电子系统的关键。随着集成技术和互连技术的发展,新的串口技术&#xf…

【QT+QGIS跨平台编译】之七十七:【QGIS_Gui跨平台编译】—【错误处理:字符串错误】

文章目录 一、字符串错误二、处理方法三、涉及到的文件一、字符串错误 常量中有换行符错误:(也有const char * 到 LPCWSTR 转换的错误) 二、处理方法 需要把对应的文档用记事本打开,另存为 “带有BOM的UTF-8” 三、涉及到的文件 src\gui\qgsadvanceddigitizingdockwidge…

ClickHouse中的设置的分类

ClickHouse中的各种设置 ClickHouse中的设置有几百个,下面对这些设置做了一个简单的分类。

【Godot 4.2】常见几何图形、网格、刻度线点求取函数及原理总结

概述 本篇为ShapePoints静态函数库的补充和辅助文档。ShapePoints函数库是一个用于生成常见几何图形顶点数据(PackedVector2Array)的静态函数库。生成的数据可用于_draw和Line2D、Polygon2D等进行绘制和显示。因为不断地持续扩展,ShapePoint…

Orbit 使用指南 03 | 与刚体交互 | Isaac Sim | Omniverse

如是我闻: “在之前的指南中,我们讨论了独立脚本( standalone script)的基本工作原理以及如何在模拟器中生成不同的对象(prims)。在指南03中,我们将展示如何创建并与刚体进行交互。为此&#xf…

机器学习周记(第三十周:文献阅读-SageFormer)2024.3.11~2024.3.17

目录 摘要 ABSTRACT 1 论文信息 1.1 论文标题 1.2 论文摘要 1.3 论文背景 2 论文模型 2.1 问题描述 2.2 模型信息 2.2.1 Series-aware Global Tokens(序列感知全局标记) 2.2.2 Graph Structure Learning(图结构学习) …

大数据面试题之SQL题

大数据面试题之SQL题 1.有一个录取学生人数表,记录的是每年录取学生人数和入学学生的学制 以下是表结构: CREATE TABLE admit ( id int(11) NOT NULL AUTO_INCREMENT, year int(255) DEFAULT NULL COMMENT ‘入学年度’, num int(255) DEFAULT NULL COMM…

交流互动系统|基于springboot框架+ Mysql+Java+Tomcat的交流互动系统设计与实现(可运行源码+数据库+设计文档)

推荐阅读100套最新项目 最新ssmjava项目文档视频演示可运行源码分享 最新jspjava项目文档视频演示可运行源码分享 最新Spring Boot项目文档视频演示可运行源码分享 2024年56套包含java,ssm,springboot的平台设计与实现项目系统开发资源(可…

【医学图像处理】ECAT和HRRT格式转nii格式【超简单】

之前从ADNI上下载PET数据的时候发现有许多数据的格式不是DICOM的而是ECAT或者是HRRT格式,这对原本就少的PET数据是血上加霜啊。 当然只使用DICOM格式的数据也会得到不少的数据,我一开始也是只使用DICOM格式的样本,后来为了得到更多的数据&a…

2024年值得创作者关注的十大AI动画创新平台

别提找大型工作室制作动画了。如今,AI平台让我们就可以轻松制作动画。从简单的文本生动画功能到复杂的角色动作,这些平台为各种类型的创作者提供了不同的功能。 AI已经有了长足的发展,现在它可以理解复杂的人类动作和艺术意图,将简单的输入转化成丰富而详细的动画。 下面…

RoketMQ主从搭建

vim /etc/hosts# IP与域名映射,端口看自己的#nameserver 192.168.126.132 rocketmq-nameserver1 192.168.126.133 rocketmq-nameserver2# 注意主从节点不在同一个主机上 #broker 192.168.126.132 rocketmq-master1 192.168.126.133 rocketmq-master2#broker 192.168…

HarmonyOS(鸿蒙)不再适合JS语言开发

ArkTS是鸿蒙生态的应用开发语言。它在保持TypeScript(简称TS)基本语法风格的基础上,对TS的动态类型特性施加更严格的约束,引入静态类型。同时,提供了声明式UI、状态管理等相应的能力,让开发者可以以更简洁、…

用Python 3 开发的摄像头拍照程序

在当今数字化的世界中,使用摄像头进行拍照已成为日常生活的重要组成部分。无论是用于个人用途还是专业用途,能够使用电脑摄像头轻松拍照都是一项有用的技能。本文将指导您使用 Python 3 编写一个简单的程序,让您能够使用电脑摄像头拍照并将其…

如果网络不好 如何下载huggingface上的模型

很多朋友网络不太好,有时候上不了huggingface这样的国外网站; 或者网络流量不太够,想要下载一些stable diffusion模型,或者其他人工智能的大模型的时候,看到动辄几个G的模型文件,不太舍得下载;…

9. 综合案例-ATM系统 (1~7节知识综合练习)

ATM系统_综合大练习 今天的任务是对之前所有的学习的知识, 进行一个综合性的大练习. 老师说的好, 键盘敲烂 这个项目我写了大量的注释给大家参考, 如果有同学是跟着我的系列学习的, 一定动手练一练. 下面的代码只要按着敲是可以直接运行起来的, 我也把完整代码上传到了CSDN上…

Fritzing 简单使用

文章目录 1 Fritzing 资源2 Fritzing 简单使用3 添加自已的元器件3.1 面包板3.1.1 新建面包板 svg 文件3.1.2 新建面包板 3.2 原理图3.3 PCB3.4 图标3.5 使用 1 Fritzing 资源 1)官网: 开源的电子设计和原型平台:https://fritzing.org/免费开…

机试:砍树修路

问题描述 代码示例: //一坐标轴表示某道路,从0开始 到L,整数位置上都种有一颗树。现在该路修建地铁,要砍掉铁路线路上的树木。例如:L等于10,铺设4条铁路,坐标是1到2,2到3,2到8,3到…