CAS 机制的实现原理分析

        在 synchronized 中很多地方都用到了CAS机制,它的叫法有很多,比如CompareAndSwap、CompareAndExchange、CompareAndSet,它是一个能够进行比较和替换的方法,这个方法能够在多线程环境下保证对一个共享变量进行修改时的原子性不变。
        为了更好地理解CAS 机制,我们来看下面这个例子,下面这个例子演示了一个对成员变量i进行累加的过程。

public class CasExample {public volatile int i;public synchronized void incr() {i++;}
}

        在不增加synchronized同步锁的情况下,incr()方法一定不是线程安全的,也就是说它无法保证原子性,但是增加锁又会导致性能问题,有没有更好的方式呢?
        这个时候我们想到了一种乐观锁机制:在线程调用i++之前,先判断i的值和之前读取的i的预期值是否相等。如果相等,则说明i的值没有被其他线程修改过,这个时候可以正常修改;否则,表示修改过,就要重新读取最新的i的值进行累加。
        按照乐观锁的思想修改后,大概就变成了下面这种结构,每次调用incr()方法时,都传递一个之前读取的i的预期值expect,如果相等就进行i++操作。

public class CasExample {public volatile int i;public void incr(int expect){if(i==expect) {i++;}
}

        但是这里存在一个问题,if语句的判断和i++指令并不是原子的,也就是说当多个线程同时执行到i==expect 这个判断条件时,初始加载的expect都是0,这会导致多个线程同时满足条件,最终还是会导致原子性问题。
        CAS就是解决这个问题的方法,如图所示,该图表示通过CAS对变量V进行原子更新操作。CAS方法中会传递三个参数,第一个参数V表示要更新的变量,第二个参数E表示期望值,第三个参数U表示更新后的值。更新的方式是,如果V=E,表示预期值和实际值相等,则将修改成U并返回true,否则修改失败返回 false


        在Java 中的 Unsafe类中提供了 CAS方法,针对int类型变量的CAS方法定义如下。

public final native boolean compareAndSwapInt(0bject o, long offset, int expect, int update);

        从方法定义中可以看到,它有四个参数:

  • o,表示当前的实例对象。
  • offset,表示实例变量的内存地址偏移量。
  • expect,表示预期值。
  • update,表示要更新的值。

        expect 和 update 比较好理解,offset表示目标变量X在实例对象0中内存地址的偏移量。简单来说,在预期值expect要和目标变量X进行比较是否相等的判断中,目标变量X的值就是通过该偏移量从内存中获得的。
 

CAS 在 AtomicInterger 中的应用

为了更好地理解CAS,我们以AtomicInteger为例来进行说明,AtomicInteger是一个能够保证原子性的Integer对象,也就是说,对于计+类的操作,可以使用AtomicInteger来保证原子性,使用方法如下。

public AtomicInteger atomicInteger = new AtomicInteger(e); 
public void add(){atomicInteger.getAndIncrement();
}

        getAndIncrement()是用来实现原子累加的方法,每调用一次会在原来值的基础上+1,这个过程采用了 CAS机制来保证原子性。
        下面来看一下getAndIncrement()方法的定义。

public final int getAndIncrement() {return unsafe.getAndAddInt(this, value0ffset,1);
}

        其中,valueOffset表示AtomicInteger中的成员变量value在内存中的偏移量,后续会用它直接从内存中读取value属性当前的值,valueOffset的初始化方法如下。

private static final Unsafe unsafe = Unsafe.getUnsafe(); 
private static final long valueOffset;static {try{valueOffset = unsafe.objectField0ffset(AtomicInteger.class.getDeclaredField("value"));} catch (Exception ex) { throw new Error(ex);}
}
private volatile int value;


valueOffset 用到了unsafe.objectFieldOffset()方法,获取 value 字段在AtomicInteger.class 中的偏移量。
结合这段代码的分析,对前面提到的o和 offset这两个字段的含义就不难理解了。在CAS 中,我们需要通过expect去和某个字段的值进行比较,而expect比较的目标值就是通过 offset找到某个字段在内存中的实际值(在AtomicInteger中是指value字段),如果相等,就修改成update 并返回true,否则返回false
下面来看一下unsafe.getAndAddInt的定义代码。

public final int getAndAddInt(Object o, long offset, int n) {int v; do{v = this.getIntVolatile(o, offset);} while(!this.compareAndSwapInt(o,offset,v,v+n)); return var5;
}

代码实现逻辑分析如下:

  • “v = this.getlntVolatile(o, offset);” 表示根据value在对象o的偏移量来获得当前的值v。
  • 使用compareAndSwapInt()方法实现比较和替换,如果value当前的值和v相等,说明数据没有被其他线程修改过,则把value修改成 v+n。
  • 这里采用了循环来实现,原因想必大家能猜测到。如果compareAndSwapInt()方法执行失败,则说明存在线程竞争,但是当前的方法是进行原子累加,所以必须要保证成功,为了达到这个目的,就只能不断地循环重试,直到修改成功后返回。


        整体来说,CAS 就是一种基于乐观锁机制来保证多线程环境下共享变量修改的原子性的解决方案。前面分析的案例虽然是在Java中的应用场景,但是它本质上和synchronized同步锁中用到的 CAS 是相同的,我们来看一下Unsafe类中CAS 的定义。

public final native boolean compareAndswapInt(0bject o, long offset, int expect, int update);

        compareAndSwapInt()是一个native方法,该方法是在JVM中定义和实现的。

CAS 实现自旋锁

        在本blog中很多地方都会提到自旋锁,那么什么是自旋锁呢?
        我们知道,在synchronized同步锁中,没有竞争到锁的线程必须要等待,直到获得锁资源的线程释放锁,才会唤醒处于锁等待的线程,而这个过程会涉及从用户态到内核态的切换带来的性能开销。在存在竞争的情况下,我们能否通过固定次数的重试,在线程进入锁等待状态之前占用锁资源呢?基于这个原因就产生了自旋锁。
        所谓自旋锁就是当一个线程在抢占锁资源时,如果锁已经被其他线程获取,那么该线程将会循环不断地判断及尝试抢占锁资源,在这个过程中该线程一直保持运行状态,不会造成上下文切换带来的性能损耗。但是自旋锁也有缺点,如果获得锁资源的线程一直没有释放,那么当前线程就会一直重试从而造成CPU资源的浪费。因此,在synchronized中会用到固定次数的自旋和自适应自旋。
        实现自旋锁的方式比较简单,需要满足如下两个条件。

  • 通过for(;;)循环不断循环重试。
  • 通过一个线程安全的操作去尝试抢占资源,而CAS 就是很好的方法,CAS是一个满足原子操作的方法,它的返回值true/false可以很好地判断当前线程竞争的结果。

        AtomicInteger中的getAndAddInt()方法其实就是一种自旋,通过一个do...while循环不断对 value 进行累加,直到累加成功便返回。

public final int getAndAddInt(0bject o, long offset, int n){int v; do {v = this,getIntVolatile(o, offset);} while(!this.compareAndSwapInt(o, offset,v,v+n)); return var5;
}

        在JVM的 synchronized的重量级锁实现中,它的白旋实现采用的是for(;;)循环,然后在该循环中通过 Atomic::cmpxchg_ptr进行CAS来抢占锁资源。

int ObjectMonitor::TryLock (Thread * Self) { for(;;) {void * own = _owner;if (own != NULL) return 0;if (Atomic::cmpxchg_ptr (Self,&_owner, NULL) == NULL) {assert (_recursions ==0, "invariant"); assert (_owner == Self, "invariant"); return 1 ;}if (true) return -1;}
}

CAS 在 JVM 中的实现原理分析

读者应该对CAS如何解决原子性的问题还存在比较多的疑惑。

        举个例子,如果多个线程调用CAS,并且多个线程都去执行预期值与实际值的判断,那么应该还存在原子性问题才对。除非当线程在执行offset偏移量的值和expect进行比较时加锁,保证在同一时刻只允许一个线程来判断。
        带着这个疑惑,我们从源码层面做一个分析,由于源码是JVM层面的C++代码实现,所以笔者会对核心逻辑做一个说明,以帮助读者理解。
基于comparcAndSwapInt(方法,在JVM源码中的unsafe.cpp文件中找到该方法的定义如下。

//UNSAFE_ENTRY 表示一个宏定义
//obj/offset/e/x 分别对应Java中定义的compareAndSwapInt()方法的入参,这里不做复述
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))UnsafeWrapper("Unsafe_CompareAndSwapInt"); oop p=JNIHandles::resolve(obj);jint* addr = (jint *) index_oop_from_field_offset_long(p, offset); return (jint)(Atomic::cmpxchg(x, addr, e)) == e; 
UNSAFE_END


代码解读如下。

  • “oopp = JNIHandles::resolve(obj);”,这个方法是把Java对象引用转化为JVM中的对象实例。
  • “ jint* addr = (jint *) index_oop_from_ field offset_long(p, offset);”,根据偏移量 offiset 计算 value 的地址。
  • “(Atomic::cmpxchg(x, addr,e))”,比较addr和c是否相等,如果相等就把x赋值到目标字段,该方法会返回修改之前的目标字段的值。

        Atomic::cmpxchg()方法的定义在atomic.epp文件中,代码如下。

unsigned Atomic::cmpxchg(unsigned int exchange_value,volatile unsigned int* dest, unsigned int compare_value) {assert(sizeof(unsigned int) == sizeof(jint), "more work to do" );return (unsigned int)Atomic::cmpxchg((jint)exchange_value,(volatile jint*)dest,(jint)compare_value);
}


        该方法并没有定义具体的实现。其实,对于CAS操作,不同的操作系统和CPU架构,其保证原子性的方法可能会不一样,而JVM本身是跨平台的语言,它需要在任何平台和CPU架构下都保证一致性。因此,Atomic::cmpxchgO方法会根据不同的操作系统类型和CPU架构,在预编译阶段确定调用哪个平台下的重载,图展示的是JVM源码中定义的多个平台的重载。


        以 Linux系统为例,当定位到atomic_linux_x86.inline.hpp文件时,Atomic:.cmpxchg的具体实现方法如下。

inline jint    Atomic::cmpxchg    (jint    exchange_value,volatile jint*    dest,    
jint compare_value) { int mp = os::is_MP();__asm__volatile (LOCK_IF_MP(%4) "cmpxchg1 %1, (%3)":"=a"(exchange_value): "r" (exchange_value),"a" (compare_value),"r" (dest),"r"(mp):"cc","memory");return exchange_value;
}


        代码说明如下。

  • mp(multi-processor),os::is_MPO用于判断是否是多核CPU。
  • __asm_表示内嵌汇编代码。
  • volatile用于通知编译器对访问该变量的代码不再进行优化。
  • LOCK_IF_MP(%4)表示如果CPU是多核的,则需要为compxchgl指令增加一条Lock指令。
  • 具体的执行过程是,先判断寄存器中的compare_value变量值是否和dest地址所存储的值相等,如果相等,就把exchange_value的值写入dest 指向的地址。

        总的来说,上面代码的功能是基于汇编指令cmpxchg1从主内存中执行比较及替换的操作来实现数据的变更。但是,在多核心CPU的情况下,这种方式仍然不是原子的,所以为了保证多核 CPU下执行该指令时的原子性,会增加一个Lock指令。Lock翻译成中文就是锁的意思,按照前面的猜想,CAS底层必然用到了锁的机制,否则无法实现原子性,因此这个猜想被证实是对的。
        Lock 的作用有两个:

  • 保证指令执行的原子性。
  • 禁止该指令与其前后的读和写指令重排序。

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

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

相关文章

CentOS 编译安装TinyXml2

安装 TinyXml2 Git 源码下载地址:https://github.com/leethomason/tinyxml2 步骤1:首先,你需要下载tinyxml2的源代码。你可以从Github或者源代码官方网站下载。并上传至/usr/local/source_code/ 步骤2:下载完成后,需要将源代码解…

『力扣刷题本』:合并两个有序链表(递归解法)

一、题目 将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 示例 1: 输入:l1 [1,2,4], l2 [1,3,4] 输出:[1,1,2,3,4,4]示例 2: 输入:l1 [], l2 [] 输出&#x…

Python---练习:使用for循环实现用户名+密码认证

案例: 用for循环实现用户登录 ① 输入用户名和密码 ② 判断用户名和密码是否正确(usernamelaowang,passwordlw123) ③ 登录仅有三次机会,超过3次会报错 思考: 用户登陆情况有3种: ① 用户名错误(此时…

Python OpenCV将n×n的小图拼接成m×m的大图

Python OpenCV将nn的小图拼接成mm的大图 前言前提条件相关介绍实验环境n \times n的小图拼接成m \times m的大图代码实现 前言 由于本人水平有限,难免出现错漏,敬请批评改正。更多精彩内容,可点击进入Python日常小操作专栏、OpenCV-Python小…

J2EE项目部署与发布(Windows版本)

一、单机项目 1.将项目共享到虚拟机 2.解压并将war包放入tomcat 3.运行tomcat并查看该项目的数据库配置 4.数据库导入脚本 先创建一个符合项目数据库配置的数据库名称 然后就是将项目脚本数据传输过去即可,如下: 项目数据传输过来了之后,我们…

分组卷积的思想神了

大家好啊,我是董董灿。 最近,分组卷积帮我解决了一个大忙,事情是这样的。 这几天遇到一个头疼的问题,就是要在某一芯片上完成一个神经网络的适配,这个神经网络中卷积居多,并且有一些卷积的通道数很大&…

React之服务端渲染

一、是什么 在SSR中 (opens new window),我们了解到Server-Side Rendering ,简称SSR,意为服务端渲染 指由服务侧完成页面的 HTML 结构拼接的页面处理技术,发送到浏览器,然后为其绑定状态与事件,成为完全可…

番外8.2 --- 后续

### 01:dd命令:在新挂载点创建swap文件大小10MB;(dd if/dev/zero of/swap bs1024 count10240) 02:给swap建立文件系统,将其分属到swap文件(mkswap /swap; swapon /swap &…

【linux系统】服务器安装Pycharm

文章目录 安装pycharm步骤1. 进入pycharm官网2. 上传到服务器3. 安装过程 摘要:pycharm是Python语言的图形化开发工具。因为如果在Linux环境下的Python shell 中直接进行编程,其无法保存与修改,在大型项目当中这是很不方便的,而py…

KV STUDIO的安装与实践(一)

目录 什么是KV STUDIO? 如何安装KV STUDIO? 如何学习与使用KV STUDIO(在现实中的应用)? 应用一(在现实生活中机器内部plc的读取与替换) 读取 KV STUDIO实现显示器的检测!&#…

Android拖放startDragAndDrop拖拽onDrawShadow动态添加View,Kotlin(3)

Android拖放startDragAndDrop拖拽onDrawShadow动态添加View,Kotlin(3) import android.content.ClipData import android.graphics.Canvas import android.graphics.Point import android.os.Bundle import android.util.Log import android.…

北邮22级信通院数电:Verilog-FPGA(7)第七周实验(1):带使能端的38译码器全加器(关注我的uu们加群咯~)

北邮22信通一枚~ 跟随课程进度更新北邮信通院数字系统设计的笔记、代码和文章 持续关注作者 迎接数电实验学习~ 获取更多文章,请访问专栏: 北邮22级信通院数电实验_青山如墨雨如画的博客-CSDN博客 关注作者的uu们可以进群啦~ 目录 方法一&#xff…

泛微OA之获取每月固定日期

文章目录 1.需求及效果1.1需求1.2效果 2. 思路3. 实现 1.需求及效果 1.1需求 需要获取每个月的7号作为需发布日期,需要自动填充1.2效果 自动获取每个月的七号2. 思路 1.功能并不复杂,可以用泛微前端自带的插入代码块的功能来实现。 2.将这需要赋值的…

LVS集群-NAT模式

集群的概念: 集群:nginx四层和七层动静分离 集群标准意义上的概念:为解决特定问题将多个计算机组合起来形成一个单系统 集群的目的就是为了解决系统的性能瓶颈。 垂直扩展:向上扩展,增加单个机器的性能,…

【java学习—九】工厂方法FactoryMethod(6)

文章目录 1. 概念2. 实际的应用 1. 概念 FactoryMethod 模式是设计模式中应用最为广泛的模式,在面向对象的编程中,对象的创建工作非常简单,对象的创建时机却很重要。 FactoryMethod 解决的就是这个问题,它通过面向对象的手法&…

BUUCTF zip伪加密 1

BUUCTF:https://buuoj.cn/challenges 题目描述: 下载附件,得到一个zip压缩包。 密文: 解题思路: 1、刚开始尝试解压,看到了flag.txt文件,但需要解压密码。结合题目,确认这是zip伪加密&#…

Makefile 基础教程:从零开始学习

在软件开发过程中,Makefile是一个非常重要的工具,它可以帮助我们自动构建程序,管理程序依赖关系,提高开发效率。本篇博客将从基础开始,介绍Makefile的相关知识,帮助大家快速掌握Makefile的使用方法 Makefil…

Corel Products Keygen-X-FORCE 2023(Corel会声会影2023注册机)

Corel All Products Universal Keygens通用注册机是一款非常实用的激活工具,专门用于激活Corel全系列产品。尤其是被广泛使用的CorelDRAW作图软件和Corel VideoStudio会声会影视频编辑处理软件。小编也是一直关注由X-Force团队制作的注册机,目前已更新至…

kubectl资源管理命令-陈述式

目录 一、陈述式对象管理 1、基本概念 2、基础命令使用 3、基本信息查看(kubectl get) 4、增删等操作 5、登录pod中的容器 6、扩容缩容pod控制器的pod 7、删除副本控制器 二、创建项目实例 1、创建 kubectl create命令 2、发布 kubectl …

MySQL-DQL【数据查询语言】(图码结合)

作者:chlorine 专栏:数据库_chlorine5的博客-CSDN博客 MySQL——DDL:DDL ——数据定义语言 MySQL——DML:DML——数据操作语言 目录 一.DQL的定义 二.DQL—语法 三.DQL—基础查询(SELECT.. FROM) 👉查询多个字段 👉设置别名 …