cas的实现原理,java中cas机制的原理

  cas的实现原理,java中cas机制的原理

  java基础教程栏目介绍分析Java CAS

  

  如何解决写爬虫IP受阻的问题?立即使用。

  推荐(免费):java基础教程

  1、简介

  CAS的全称是compare and swap,是多线程环境下实现同步的一种机制。CAS操作包含三个操作数——内存位置、期望值和新值。CAS的实现逻辑是将内存位置的值与期望值进行比较,如果相等,则用新值替换内存位置的值。如果不是,什么都不做。

  在Java中,Java并不直接实现CAS,CAS的相关实现是通过C内嵌汇编来实现的。Java代码只能通过JNI调用。我将在第3章分析实现细节。

  要说CAS操作的流程,并不是很难。但是光靠上面的描述是不够的。接下来我会介绍一些其他的背景知识。有了这些背景知识,我们就能更好地理解下面的内容。

  2.背景介绍

  众所周知,CPU通过总线和内存传输数据。多核时代,多个核心通过相同的总线、内存等硬件进行通信。如下图:

  图片来源:《深入理解计算机系统》

  上图是比较简单的计算机结构图。虽然简单,但足以说明问题。在上图中,CPU通过蓝色箭头标记的两条总线与内存通信。我们来考虑一个问题。如果CPU的多个内核同时对同一块内存进行操作,如果不加以控制会造成什么样的错误?这里简单说明一下,假设core 1通过32位带宽的总线将64位数据写入内存,core 1要写两次才能完成整个操作。如果内核1首次写入32位数据,内核2将从内核1写入的存储位置读取64位数据。由于核1还没有将所有的64位数据完全写入内存,所以核2从这个内存位置开始读取数据,所以读取的数据一定是混乱的。

  但是,这个问题其实不用担心。从《英特尔开发人员手册》中,我们可以知道,从奔腾处理器开始,英特尔处理器将保证以原子的方式读写与64位边界对齐的四字。

  根据上面的描述,我们可以得出结论,英特尔处理器可以保证单次访问存储器对齐指令以原子方式执行。但是如果是两次访问内存的指令呢?答案是不能保证。例如,增量指令inc dword ptr [.]相当于DEST=DEST 1。该指令包含三个操作:读-改-写,涉及两次内存访问。考虑一种情况,其中值1被存储在存储器中的指定位置。现在CPU的两个核心同时执行这个指令。两个内核交替执行的过程如下:

  内核1从指定的存储器位置读取值1,并将其载入寄存器。内核2从指定的存储器位置读取值1,并将其载入寄存器。内核1递减寄存器中的值1。内核2递减寄存器中的值1。内核1将修改后的值写回存储器。内核2将修改后的值写回存储器。执行上述过程后,内存中的最终值是2,但我们期望的是3。这是一个问题。为了处理这个问题,有必要避免两个或多个内核同时操作同一个存储区域。那么如何避免呢?这就要介绍这篇文章的主角了——锁前缀。有关此指令的详细描述,请参考英特尔开发人员手册第2卷指令集参考,第3章指令集参考A-L。我在此引用其中一条,如下所示:

  以上描述的重点已经用粗体标出。在多处理器环境中,LOCK#信号可以确保处理器独占使用一些共享内存。可以在以下指令前添加锁定:

  ADD、ADC、AND、BTC、BTR、BTS、CMPXCHG、CMPXCH8B、CMPXCHG16B、DEC、INC、NEG、NOT、OR、SBB、SUB、XOR、XADD和XCHG。

  通过在inc指令之前添加锁定前缀,该指令可以是原子的。当多个内核同时执行同一个inc指令时,它们会以串行方式执行,从而避免上述情况。所以这里还有一个问题。锁前缀是如何保证内核独占一定内存区域的?答案如下:

  在英特尔处理器中,有两种方法可以确保处理器的某个内核独占某个内存区域。第一种方法是锁定总线,让一个内核独占使用总线,但这种方法开销太大。总线被锁定后,其他内核无法访问内存,可能会导致其他内核短时间停止工作。第二种方法是锁定缓存,如果某处的内存数据缓存在处理器缓存中。处理器发出的LOCK#信号并不锁定总线,而是锁定缓存行对应的内存区域。当此内存区域被锁定时,其他处理器无法在其上执行相关操作。与锁定总线相比,锁定缓存的成本显然更小。有关总线锁和高速缓存锁的详细描述,请参考英特尔开发人员手册第8章多处理器管理。

  3.源码分析

  有了以上的背景知识,现在我们就可以悠闲地阅读CAS的源代码了。本章内容将分析java.util.concurrent.atomic包下原子类AtomicInteger中的compareAndSet方法,相关分析如下:

  公共类AtomicInteger扩展数字实现java.io.Serializable {

  //安装程序使用Unsafe.compareAndSwapInt进行更新

  private static final Unsafe Unsafe=Unsafe . get Unsafe();

  私有静态最终长值偏移量;

  静态{

  尝试{

  //计算类对象中变量值的偏移量

  value offset=unsafe . objectfield offset

  (atomic integer . class . getdeclaredfield( value ));

  } catch(Exception ex){ throw new Error(ex);}

  }

  私有可变int值;

  public final boolean compare andset(int expect,int update) {

  /*

  * compareAndSet实际上只是一个外壳,主要逻辑封装在Unsafe中

  * compareAndSwapInt方法

  */

  返回unsafe.compareAndSwapInt(this,valueOffset,expect,update);

  }

  //.

  }

  公共最终类不安全{

  //compareAndSwapInt是原生类型方法,继续读。

  public final native boolean compareAndSwapInt(Object o,long offset

  应为int,

  int x);

  //.

  }//unsafe.cpp

  /*

  *这个看起来不像函数,但是不用担心,这不是重点。UNSAFE_ENTRY和UNSAFE_END是宏,

  *在预编译期间将被替换为真实代码。下面的jboolean、jlong和jint是一些类型定义(typedef):

  *

  * jni.h

  * typedef无符号char jboolean

  * typedef无符号短jchar

  * typedef short jshort

  * typedef float jfloat

  * typedef double jdouble

  *

  * jni_md.h

  * typedef int jint

  * #ifdef _LP64 //64位

  * typedef long jlong

  * #其他

  * typedef long long jlong

  * #endif

  * typedef有符号字符jbyte

  */

  UNSAFE_ENTRY(jboolean,UNSAFE _ CompareAndSwapInt(JNIEnv * env,jobject unsafe,jobject obj,jlong offset,jint e,jint x))

  unsawewrapper( Unsafe _ CompareAndSwapInt );

  OOP p=JNI handles:resolve(obj);

  //根据偏移量,计算值的地址。这里的偏移量是AtomaicInteger中的值offset。

  jint * addr=(jint *)index _ OOP _ from _ field _ offset _ long(p,offset);

  //调用Atomic中的函数cmpxchg,该函数在Atomic.hpp中声明

  return (jint)(Atomic:cmpxchg(x,addr,e))==e;

  不安全_结束

  //atomic.cpp

  无符号Atomic:cmpxchg(无符号int exchange_value,

  可变无符号int* dest,无符号int compare_value) {

  assert(sizeof(unsigned int)==sizeof(jint),更多工作要做);

  /*

  *根据操作系统的类型,调用不同平台下的重载函数。这个编译器将决定在预编译期间调用哪个平台。

  *功能。的相关预编译逻辑如下:

  *

  * atomic.inline.hpp:

  * #include runtime/atomic.hpp

  *

  * //Linux

  * # ifdef TARGET _ OS _ ARCH _ Linux _ x86

  * # include atomic _ Linux _ x86 . inline . HPP

  * #endif

  *

  *//省略一些代码

  *

  * //Windows

  * # ifdef TARGET _ OS _ ARCH _ windows _ x86

  * # include atomic _ windows _ x86 . inline . HPP

  * #endif

  *

  * //BSD

  * #ifdef TARGET_OS_ARCH_bsd_x86

  * # include atomic _ BSD _ x86 . inline . HPP

  * #endif

  *

  *接下来分析cmpxchg函数在atomic _ windows _ x86.inline.hpp中的实现。

  */

  return(unsigned int)Atomic:cmpxchg((jint)exchange _ value,(volatile jint*)dest,

  (jint)比较_值);

  }以上分析看似较多,但主要过程并不复杂。如果不纠缠于代码的细节,就更容易理解了。接下来我就来分析一下Windows平台下的Atomic:cmpxchg函数。继续读。

  //atomic _ windows _ x86 . inline . HPP

  # define LOCK _ IF _ MP(MP)_ ASM CMP MP,0 \

  __asm je L0 \

  __asm _emit0xF0 \

  __asm L0:

  inline jint Atomic:cmpxchg(jint exchange _ value,volatile jint* dest,jint compare_value) {

  InterlockedCompareExchange的替代项

  int MP=OS:is _ MP();

  _ _组件{

  移动edx,目标

  mov ecx,交换值

  mov eax,比较值

  LOCK_IF_MP(mp)

  cmpxchg dword ptr [edx],ecx

  }

  }以上代码由LOCK_IF_MP预编译标识符和cmpxchg函数组成。为了看得更清楚,我们用实际内容替换了cmpxchg函数中的LOCK_IF_MP。如下所示:

  inline jint Atomic:cmpxchg(jint exchange _ value,volatile jint* dest,jint compare_value) {

  //判断是否是多核CPU

  int MP=OS:is _ MP();

  _ _组件{

  //将参数值放入寄存器

  Mov edx,dest //注意:dest是指针类型,这里的内存地址存放在edx寄存器中。

  mov ecx,交换值

  mov eax,比较值

  //LOCK_IF_MP

  cmp mp,0

  /*

  *如果mp=0,表示线程运行在单核CPU环境中。这时,je将跳到L0标记,

  *即cmpxchg指令是在_emit0xF0指令之外直接执行的。也就是不在下面的cmpxchg指令。

  *前缀为lock。

  */

  je L0

  /*

  *0xF0是带锁定前缀的机器代码。这里不用lock,直接用机器码的形式。至于这样做

  *原因可以参考知乎的一个回答:

  * https://www.zhihu.com/question/50878124/answer/123099923

  */

  _发出0xF0

  L0:

  /*

  *比较和交换。简要解释下面的指令。熟悉汇编的朋友可以跳过下面的解释:

  * cmpxchg:即“比较和交换”指令。

  * dword:全称是双字。在x86/x64系统中,一个

  *字=2字节,双字=4字节=32位

  * ptr:全称是pointer,与前面的dword连用,表示访问的内存单元是两个字的单元。

  * [edx]: [.]表示内存单元,edx是寄存器,dest指针值存储在edx中。

  *然后[edx]表示存储地址为dest的存储单元。

  *

  *此指令的含义是将eax寄存器(compare_value)中的值与[edx]双字存储单元中的值进行比较。

  *进行比较,如果相同,则将值(exchange_value)存储在[edx]存储单元的ecx寄存器中。

  */

  cmpxchg dword ptr [edx],ecx

  }

  }这里就完成了CAS的实现过程,CAS的实现离不开处理器的支持。上面这么多代码,其实核心代码是一个带锁前缀的cmpxchg指令,即lock cmpxchg dword ptr [edx],ecx。

  4.ABA 问题

  说到CAS,基本上就要说到CAS里的ABA了。CAS包括三个步骤,即“读-比较-写-回”。考虑一种情况,线程1和线程2同时执行CAS逻辑,两个线程的执行顺序如下:

  时间1:线程1执行读取操作,获取原始值A,然后线程被切换走。时间2:线程2执行CAS操作,将原始值从A改为B .时间3:线程2再次执行CAS操作,将原始值从B改为A .时间4:线程1恢复操作,将compareValue与原始值进行比较,发现两个值相等。然后将newValue写入内存,完成CAS操作,如上述过程所示。线程1不知道原来的值被修改了,看起来也没有改变什么,所以会继续执行进程。对于ABA问题,通常的解决方法是为每个CAS操作设置版本号。java.util.concurrent.atomic包下提供了原子类AtomicStampedReference,可以处理ABA问题。具体实现这里就不分析了,有兴趣的朋友可以自己看看。

  5.总结

  至此,本文终于要结束了。虽然CAS本身的原理,包括实现都不是很难,但是写起来真的不容易。涉及到一些基础知识。虽然可以理解,但是解释清楚还是有点困难。由于我的底层知识不足,上面的一些分析难免会出错。所以如有错误,请轻喷。当然,最好能说明是怎么错的。谢谢你。

  好了,本文到此结束。感谢阅读,再见。

  附录

  在前面的源代码分析部分中使用的几个文件。在这里,张贴路径。帮你索引,如下:

  以上是

文件名路径
Unsafe.javaopenjdk/jdk/src/share/classes/sun/misc/Unsafe.java
unsafe.cppopenjdk/hotspot/src/share/vm/prims/unsafe.cpp
atomic.cppopenjdk/hotspot/src/share/vm/runtime/atomic.cpp
atomic_windows_x86.inline.hppopenjdk/hotspot/src/os_cpu/windows_x86/vm/atomic_windows_x86.inline.hpp
介绍Java CAS原理分析的详细内容。请多关注我们的其他相关文章!

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: