14.CAS原理

文章目录

  • CAS原理
    • 1.什么是CAS
    • 2.Unsafe类中的CAS方法
      • 2.1.获取UnSafe实例
      • 2.2.调用UnSafe提供的CAS方法
      • 2.3.调用Unsafe提供的偏移量相关
      • 2.4.CAS无锁编程
      • 2.4.1.使用cas进行无锁安全自增案例

CAS原理

由于JVM的synchronized重量级锁设计操作系统内核态下的互斥锁的使用,其线程的阻塞和唤醒在内核态和用户态频繁切换,导致重量级锁的开销很大,性能低。JVM为synchronized提供了轻量级锁,通过CAS(compare and swap)比较交换,进行自旋抢锁,CAS是CPU指令级别的原子操作,并且处于用户状态下,开销比较小。

下面我们来了解一下什么是CAS

1.什么是CAS

JDK 5 所增加的 JUC(java.util.concurrent)并发包对操作系统的底层的CAS原子操作进行了封装,为上层Java程序提供了CAS操作的API

CAS(Compare and Swap)是一种并发编程中的原子操作,用于实现多线程环境下的无锁同步。CAS操作包含三个参数:内存位置(或者说是要操作的变量的引用)、期望值新值

CAS操作的执行过程如下:

  1. 首先,读取内存位置的当前值,这是期望值。
  2. 然后,将期望值与内存位置的当前值进行比较。如果相等,则说明内存位置的值没有被其他线程修改,可以进行更新操作。
  3. 如果相等,将内存位置的当前值修改为新值。如果不相等,说明内存位置的值已经被其他线程修改,CAS操作失败。
  4. 最后,CAS操作返回执行结果,通常是一个布尔值,表示操作是否成功。

CAS操作是原子的,意味着整个操作在执行期间不会被其他线程中断。如果多个线程同时执行CAS操作,只有一个线程会成功,其他线程会根据操作结果进行重试或进行其他处理。

2.Unsafe类中的CAS方法

Unsafe是位于sum.misc包下面的一个类,主要提供一些执行级别低,不安全的底层操作逻辑。如直接访问系统内存资源,自主管理内存资源等。

Unsafe中的大量方法都是原生(naticve)方法,基于C++语言实现,这些方法在提升Java运行效率上起了很大作用,但是在一般的开发中不会涉及此类,Java官方也不建议直接在应用程序中使用。

操作系统层面的CAS是一条CPU原子指令(compxchg)指令,UnSafe提供的CAS方法直接通过native方式(封装的C++代码)调用了底层CPU指令的compxchg。

在Java应用层,CAS操作主要是通过调用sun.misc.Unsafe类中的方法来实现的。Unsafe类提供了一些底层的、直接操作内存和执行CAS操作的方法。下面是使用Unsafe类进行CAS操作的一般流程:

  1. 获取Unsafe实例
    Unsafe类的构造函数是私有的,因此无法直接实例化它。通常可以通过反射或者调用Unsafe.getUnsafe()方法来获取Unsafe的实例。
  2. 获取要操作的变量的偏移量
    在执行CAS操作之前,需要获取要操作的变量在内存中的偏移量。偏移量表示变量相对于对象头的位置。可以使用Unsafe.objectFieldOffset()方法来获取变量的偏移量。
  3. 执行CAS操作
    通过调用Unsafe.compareAndSwapXXX()方法来执行CAS操作,其中XXX表示要操作的数据类型(如IntLongObject等)。compareAndSwapXXX()方法接收四个参数:要操作的对象、变量的偏移量、期望值和新值。方法会比较对象内存中偏移量处的值与期望值是否相等,如果相等,则将该位置的值更新为新值,并返回操作是否成功的布尔值。
  4. 锁定和解锁
    在执行CAS操作期间,不需要使用锁来保护共享资源,因为CAS操作本身是原子的。它使用底层硬件指令实现的原子性保证。因此,CAS操作可以避免传统的锁机制所带来的开销和竞争。

需要注意的是,使用Unsafe类进行CAS操作需要谨慎,因为直接操作内存可能会导致不安全的结果。此外,Unsafe类在Java 9中被标记为不推荐使用,并且在未来的版本中可能会被移除。因此,在实际应用中,建议使用更高级的并发工具和类库,如java.util.concurrent.atomic包中的原子类来实现线程安全的操作

2.1.获取UnSafe实例

@Test
@DisplayName("获取UnSafe实例")
public void test() {try {Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");theUnsafe.setAccessible(true);Unsafe unsafe = (Unsafe) theUnsafe.get(null);log.error("unsafe : {}",unsafe);} catch (NoSuchFieldException e) {throw new RuntimeException(e);} catch (IllegalAccessException e) {throw new RuntimeException(e);}
}

在这里插入图片描述

2.2.调用UnSafe提供的CAS方法

Unsafe类中的提供了三个CAS操作方法:compareAndSwapObject()compareAndSwapInt()compareAndSwapLong()

这三个方法的作用是原子地更新Java变量的值,只有在当前值等于期望值时才进行更新。它们的共同特点是具有volatile读写的内存语义

  • compareAndSwapObject()方法用于原子地更新对象引用类型的变量。它接收四个参数:要操作的对象、变量的偏移量、期望值和新值。如果对象内存中偏移量处的值等于期望值,则将该位置的值更新为新值,并返回操作是否成功的布尔值。

    • @ForceInline
      public final boolean compareAndSwapObject(Object o, long offset,Object expected,Object x) {return theInternalUnsafe.compareAndSetReference(o, offset, expected, x);
      }
      
  • compareAndSwapInt()方法用于原子地更新int类型的变量。它的参数和行为与compareAndSwapObject()方法类似,只是操作的数据类型不同。

    • @ForceInline
      public final boolean compareAndSwapInt(Object o, long offset,int expected,int x) {return theInternalUnsafe.compareAndSetInt(o, offset, expected, x);
      }
      
  • compareAndSwapLong()方法用于原子地更新long类型的变量。它的参数和行为与compareAndSwapObject()方法类似,只是操作的数据类型不同。

    • @ForceInline
      public final boolean compareAndSwapLong(Object o, long offset,long expected,long x) {return theInternalUnsafe.compareAndSetLong(o, offset, expected, x);
      }
      

这些CAS操作方法在底层都使用了Unsafe类内部的方法,如compareAndSetReference()compareAndSetInt()compareAndSetLong(),这些方法是基于硬件指令实现的原子操作。

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

2.3.调用Unsafe提供的偏移量相关

Unsafe类提供了两个方法来获取字段的偏移量:staticFieldOffset()objectFieldOffset()

  1. staticFieldOffset()

    @ForceInline
    public long staticFieldOffset(Field f) {if (f == null) {throw new NullPointerException();}Class<?> declaringClass = f.getDeclaringClass();if (declaringClass.isHidden()) {throw new UnsupportedOperationException("can't get field offset on a hidden class: " + f);}if (declaringClass.isRecord()) {throw new UnsupportedOperationException("can't get field offset on a record class: " + f);}return theInternalUnsafe.staticFieldOffset(f);
    }
    // 最终调用 
    private native long staticFieldOffset0(Field f);

    staticFieldOffset()方法用于获取静态字段的偏移量。它接收一个Field对象作为参数,表示要获取偏移量的字段。该方法返回一个long类型的值,表示字段在内存中的偏移量。

    在使用staticFieldOffset()方法之前,需要确保传入的字段对象不为null。方法内部还会进行一些额外的检查,例如检查字段所在的类是否是隐藏类或记录类。

  2. objectFieldOffset()

    @ForceInline
    public long objectFieldOffset(Field f) {if (f == null) {throw new NullPointerException();}Class<?> declaringClass = f.getDeclaringClass();if (declaringClass.isHidden()) {throw new UnsupportedOperationException("can't get field offset on a hidden class: " + f);}if (declaringClass.isRecord()) {throw new UnsupportedOperationException("can't get field offset on a record class: " + f);}return theInternalUnsafe.objectFieldOffset(f);
    }// 最终调用 
    private native long objectFieldOffset0(Field f);
    

    objectFieldOffset()方法用于获取对象字段的偏移量。它的使用方式和staticFieldOffset()方法类似,接收一个Field对象作为参数,并返回字段的偏移量。

    同样,使用objectFieldOffset()方法之前,需要确保传入的字段对象不为null。方法内部也会进行类的隐藏性和是否为记录类的检查。

这些偏移量可以在CAS操作中使用,通过偏移量可以直接访问和修改字段的值,而无需通过对象引用。但是需要注意的是,直接使用偏移量进行字段操作需要非常小心,因为它绕过了Java语言的访问控制机制,可能会导致不安全或破坏封装性的操作。在正常情况下,应该使用正式的访问方法(getter和setter)来访问和修改字段的值。

2.4.CAS无锁编程

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

对于CAS操作,可以通过以下三个步骤来说明其流程:

  1. 获取期望值
    CAS操作首先从内存中读取变量的当前值作为期望值。这个期望值是在进行CAS操作之前由应用程序指定的。它用于比较内存中的原值是否与期望值相等。
  2. 计算需要替换值
    如果内存中的原值与期望值相等,说明当前变量的值满足CAS操作的条件。在这种情况下,应用程序可以计算出需要替换的新值。这个新值可能基于当前的变量值和其他相关的计算逻辑。
  3. 更新值
    在CAS操作中,如果内存中的原值与期望值相等,CAS操作会将计算得到的新值尝试写入内存。这个更新操作是原子的,它通过底层的硬件指令保证了操作的原子性和线程安全性。如果更新成功,说明CAS操作成功,变量的值已被更新为新值;否则,说明在CAS操作期间其他线程修改了变量的值,CAS操作失败,需要重新尝试或执行其他的处理逻辑。

下面我们通过一个简单案例来了解一下CAS的一个流程

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

2.4.1.使用cas进行无锁安全自增案例

package com.hrfan.java_se_base.base.thread.cas.unsafe;import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sun.misc.Unsafe;import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;public class SpinLockTest {private static final Logger log = LoggerFactory.getLogger(SpinLockTest.class);private static final Unsafe unsafe = getUnsafe();private static final long valueOffset;private volatile int value = 0;private AtomicInteger failCount = new AtomicInteger(0);static {try {valueOffset = unsafe.objectFieldOffset(SpinLockTest.class.getDeclaredField("value"));log.error("cas偏移量:{}", valueOffset);} catch (NoSuchFieldException e) {throw new Error(e);}}private static Unsafe getUnsafe() {try {java.lang.reflect.Field field = Unsafe.class.getDeclaredField("theUnsafe");field.setAccessible(true);return (Unsafe) field.get(null);} catch (Exception e) {throw new RuntimeException("Failed to obtain Unsafe instance", e);}}// 自旋进行等待 直到赋值成功!public void incrementAndPrint() {// 使用Unsafe的compareAndSwapInt方法进行自增操作int oldValue, newValue;do {// 获取旧的值oldValue = unsafe.getIntVolatile(this, valueOffset);// 设置新的值newValue = oldValue + 1;} while (!unsafe.compareAndSwapInt(this, valueOffset, oldValue, newValue));}public static void main(String[] args) throws InterruptedException {SpinLockTest lock = new SpinLockTest();CountDownLatch latch = new CountDownLatch(10);for (int i = 0; i < 10; i++) {new Thread(() -> {for (int j = 0; j < 1000; j++) {lock.incrementAndPrint();}latch.countDown();}).start();}// 等待全部线程执行完毕latch.await();// 输出最终结果值log.error("最终累加值:{}", lock.value);}
}

在这里插入图片描述

注意 为什么每次的偏移量都是12呢?
在 Java 中,每个对象的开头都有一个称为 Mark Word 的特殊字段,用于存储对象的一些标记和状态信息。Mark Word 的大小通常是机器字大小的整数倍,它的确切大小会随着 JVM 的具体实现而有所不同。

在大多数情况下,Mark Word 包含了对象的哈希码、锁状态、GC 信息等。在使用 Unsafe 类进行对象操作时,通过 objectFieldOffset 方法获取到的偏移量实际上是指向了对象中某个字段的相对位置,而这个相对位置是相对于对象起始地址的偏移量。

由于 Mark Word 是对象的头部信息,它的大小会影响到对象中其他字段的偏移量。具体来说,由于 Mark Word 的存在,对象中第一个字段的偏移量通常会是 Mark Word 的大小,因此每次获取对象中字段的偏移量时,相对于对象起始地址的偏移量会是固定的,也就是 Mark Word 的大小。

因此,每次获取的偏移量都是固定的值,是 Mark Word 的大小。其实就是 value属性的内存位置紧挨着Object Header之后,所以value属性的相对偏移量都是 12

在这里插入图片描述

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

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

相关文章

二分判定+选插冒排序+归并快速堆希尔+计数排序

二分力扣题 一&#xff1a;搜索二维矩阵 74. 搜索二维矩阵 按照题意&#xff1a;直接利用二维数组转换成一维数组进行求解 方法一&#xff1a;普通等于的二分查找 class Solution { public:bool searchMatrix(vector<vector<int>>& matrix, int target) {t…

io_uring的使用示例及其解释

io_uring的使用示例及其解释 1 io_uring机制1.1 io_uring机制1.2 io_uring系统调用接口功能介绍1.2.1 io_uring_setup()&#xff1a;1.2.2 io_uring_enter()&#xff1a;1.2.3 io_uring_register()&#xff1a; 2 liburing2.1 liburing简介2.2 liburing编译2.2.1 liburing的代码…

基础ArkTS组件:导航栏组件(HarmonyOS学习第三课【3.8】)

Navigation 官方文献 Navigation 组件一般作为页面布局的根容器&#xff0c;它提供了一系列属性方法来设置页面的标题栏、工具栏以及菜单栏的各种展示样式。 Navigation 除了提供了默认的展示样式属性外&#xff0c;它还提供了 CustomBuilder 模式来自定义展示样式 说明 该…

OCR技术在历史文献数字化中的革命性作用

随着数字化技术的不断发展&#xff0c;历史文献的数字化已成为保存和传播文化遗产的重要途径。其中&#xff0c;光学字符识别&#xff08;OCR&#xff09;技术在历史文献数字化中发挥了革命性的作用&#xff0c;为研究者提供了更广阔的研究空间&#xff0c;推动了历史学研究的发…

kafka安装及收发消息

kafka需要与zookeeper配合使用&#xff0c;但是从2.8版本kafka引入kraft&#xff0c;也就是说在2.8后&#xff0c;zookeeper和kraft都可以管理kafka集群&#xff0c;这里我们依然采用zookeeper来配合kafka。 1、首先我们下载zookeeper 下载地址为 https://zookeeper.apache.org…

三. TensorRT基础入门-剖析ONNX架构并理解ProtoBuf

目录 前言0. 简述1. 执行一下我们的python程序2. ONNX是什么&#xff1f;3. onnx中的各类Proto3.1 理解onnx中的ValueInfoProto3.2 理解onnx中的TensorProto3.3 理解onnx中的NodeProto3.4 理解onnx中的AttributeProto3.5 理解onnx中的GraphProto3.6 理解onnx中的ModelProto 4. …

算法提高之单词接龙

算法提高之单词接龙 核心思想&#xff1a;dfs 预处理每两个字符串之间最短的公共部分长度 求最短公共 最终字符串是最长 dfs所有开头字符串 #include <iostream>#include <cstring>#include <algorithm>using namespace std;const int N 25;int g[N][N…

Feign 和 OpenFeign 的区别

Feign 和 OpenFeign 都是用来进行服务间调用的客户端库&#xff0c;它们旨在简化HTTP API客户端的编写过程&#xff0c;使得编写对外部服务的接口就像调用本地方法一样简单。尽管它们有相似之处&#xff0c;但也存在一些关键差异&#xff1a; 归属和演进&#xff1a; Feign 最初…

大规模 RGB LED灯控系统 Lumos:创新与智能化的融合

灯控系统&#xff1a;创新与智能化的融合 在现代照明技术不断进步的背景下&#xff0c;灯控系统的应用已经从简单的开关控制&#xff0c;发展到能够进行复杂程控操作的智能化管理。我们推出的新一代灯控解决方案&#xff0c;凭借其高度的可配置性和跨平台兼容性&#xff0c;已…

虚拟化数据恢复—误还原虚拟机快照怎么办?怎么恢复最新虚拟机数据?

虚拟化技术原理是将硬件虚拟化给不同的虚拟机使用&#xff0c;利用虚拟化技术可以在一台物理机上安装多台虚拟机。误操作或者物理机器出现故障都会导致虚拟机不可用&#xff0c;虚拟机中的数据丢失。 虚拟化数据恢复环境&#xff1a; 有一台虚拟机是由物理机迁移到ESXI上面的&a…

pikachu靶场(xss通关教程)

&#xff08;注&#xff1a;若复制注入代码攻击无效&#xff0c;请手动输入注入语句&#xff0c;在英文输入法下&#xff09; 反射型xss(get型) 1.打开网站 发现有个框&#xff0c;然后我们在框中输入一个“1”进行测试&#xff0c; 可以看到提交的数据在url处有显示&#xf…

Debian Linux 下给Nginx 1.26.0 编译增加Brotli算法支持

明月发现参考【给Nginx添加谷歌Brotli压缩算法支持】一文给出的方法&#xff0c;在Debian Linux 12.5下就一直编译失败&#xff0c;主要的错误是因为文件缺失&#xff0c;在专门又安装了apt-get install libbrotli-dev的依赖库后依然会因为文件缺失无法编译完成&#xff0c;就这…

ERP与MES与WMS集成

WMS储位管理 WMS与MES集成 (一) 打通追溯链 在拣货时&#xff0c;将配料标签与供应商的物料标签进行关联。通过配料标签达到精确追溯及防错目的。针对模糊查询&#xff0c;将工单与物料的供应商信息、仓库流转信息进行关联。 (二) WMS入库 成品(半成品)下线后&#xff0c;M…

draw.text((left, top - 15), text,font=font, fill=“green”)

这是一个Python PIL库中的方法&#xff0c;用于在图片上绘制文本。具体来说&#xff0c;它可以在指定的位置绘制指定的文本&#xff0c;并使用指定的字体、颜色等参数进行渲染。其中&#xff0c;left和top是文本绘制的左上角坐标&#xff0c;text是要绘制的文本内容&#xff0c…

齿轮滚刀刃口钝化技术简介

介绍 在滚刀的使用中发现&#xff0c;进口滚刀和国产滚刀在加工质量和寿命方面存在显著差异。经过多次比较得知&#xff0c;滚刀的使用寿命可以达到国产滚刀的两倍以上&#xff0c;而进口滚刀返回原厂磨削后的使用寿命约为新刀具的90% &#xff0c;但同样经过国内厂家磨削后&a…

【C语言项目】贪吃蛇(下)

个人主页~ 源码在Gitee仓库~ 上一篇贪吃蛇&#xff08;上&#xff09;~ 贪吃蛇 四、核心的实现游戏测试1、GameStart&#xff08;1&#xff09;控制台窗口大小和名字设置&#xff08;2&#xff09;光标隐藏&#xff08;3&#xff09;打印欢迎界面&#xff08;4&#xff09;创建…

azkaban-tools 项目介绍

本文背景 应一个用户的好心和好奇心&#xff0c;在最近水深火热的百忙之中抽时间写完了一个简短的项目介绍&#xff0c;其实就是几个azkaban的批量操作脚本&#xff0c;但在大数据集群的“运维生涯”中&#xff0c;还是帮了自己不少忙&#xff0c;也算是为了它做一个简单的回顾…

Java | Leetcode Java题解之第85题最大矩形

题目&#xff1a; 题解&#xff1a; class Solution {public int maximalRectangle(char[][] matrix) {int m matrix.length;if (m 0) {return 0;}int n matrix[0].length;int[][] left new int[m][n];for (int i 0; i < m; i) {for (int j 0; j < n; j) {if (mat…

Python3 + Appium + 安卓模拟器实现APP自动化测试并生成测试报告

这篇文章主要介绍了Python3 Appium 安卓模拟器实现APP自动化测试并生成测试报告,本文给大家介绍的非常详细&#xff0c;对大家的学习或工作具有一定的参考借鉴价值&#xff0c;需要的朋友可以参考下 本文主要分为以下几个部分 安装Python3 安装Python3的Appium库 安装Andr…

Mp3tag for Mac:音乐标签,轻松管理

还在为杂乱无章的音乐文件而烦恼吗&#xff1f;Mp3tag for Mac&#xff0c;让您的音乐库焕然一新&#xff01;它支持多种音频格式&#xff0c;批量编辑标签&#xff0c;让音乐管理变得简单高效。同时&#xff0c;自动获取在线数据库的音乐元数据&#xff0c;确保您的音乐库始终…