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.java | openjdk/jdk/src/share/classes/sun/misc/Unsafe.java |
unsafe.cpp | openjdk/hotspot/src/share/vm/prims/unsafe.cpp |
atomic.cpp | openjdk/hotspot/src/share/vm/runtime/atomic.cpp |
atomic_windows_x86.inline.hpp | openjdk/hotspot/src/os_cpu/windows_x86/vm/atomic_windows_x86.inline.hpp |
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。