从JVM 源码的角度深度剖析CAS是如何实现原子性的
CAS 介绍
CAS(Compare And Swap,比较并交换),通常指的是这样一种原子操作:针对一个变量,首先比较它的内存值与某个期望值是否相同,如果相同,就给它赋一个新值。
Unsafe类中的CAS方法
sun.misc.Unsafe 类是一个不安全的类,提供了一组用于执行底层的,不安全操作的方法。
完成java应用层的CAS操作主要涉及Unsafe方法的调用,具体如下:
(1) 获取Unsafe实例
Unsafe类是一个final修饰的不允许继承的最终类,而且其构造函数是private类型的方法,如下所示:
public final class Unsafe {
private Unsafe() {}
}
因此我们无法在外部对Unsafe进行实例化,可以通过反射的方式自定义地 获取unsafe实例的辅助方法,代码如下:
public static Unsafe getUnsafe() {
try {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
return (Unsafe) theUnsafe.get(null);
} catch (Exception e) {
throw new AssertionError(e);
}
}
(2)调用Unsafe提供的CAS方法,这些方法主要封装了底层CPU的CAS原子操作。
Unsafe 提供的CAS方法包含4个操作数-字段所在的对象、字段内存位置、预期原值,新值;在执行Unsafe的CAS方法时,这些方法首先将内存位置的值与预期值比较,如果相匹配,那么CPU 会自动将该内存位置的值更新为新值,并返回true,如果不匹配,CPU不做任何操作,并返回false。部分源码如下:
/*
* 比价并交换的原子方法
* @param o 需要操作的字段所在的对象
* @param offset 需要操作的字段的偏移量(相对的,相对于对象头)
* @param expected 期望值(旧的值)
* @param x 更新值(新的值)
* @return {@code true} 如果成功返回true
*/
public final native boolean compareAndSetInt(Object o, long offset, int expected, int x);
public final native boolean compareAndSetObject(Object o, long offset, Object expected,Object x);
public final boolean compareAndSetShort(Object o, long offset, short expected,short x) ;
Unsafe的CAS操作会将第一个参数(对象的指针、地址)与第二个参数(字段偏移量)组合在一起,计算出最终的内存操作地址。
(3)调用Unsafe提供的字段偏移量方法,这些方法用于获取对象中的字段偏移量,此偏移量值需要作为参数提供给CAS操作。
Unsafe 提供的获取字段(属性)偏移量的相关操作主要如下:
/*
* 获取静态属性Field在Class对象中的偏移量,在CAS中操作静态属性时会用到这个偏移量。
*@param f 需要操作字段的反射
*@return 字段的偏移量
/
public long staticFieldOffset(Field f);
/*
* 获取非静态Field(非静态属性)在Object实例中的偏移量,在CAS中操作对象的非静态属性时会用到这个偏移量。
*@param f 需要操作字段的反射
*@return 字段的偏移量
/
public long objectFieldOffset(Field f);
使用CAS 进行无锁编程
CAS 是一种无锁算法,该算法关键依赖两个值-期望值和新值,使用CAS进行无所编程的步骤如下:
(1)获得字段的期望值(oldValue)
(2)计算出需要替换的新值(newValue)
(3)通过CAS将新值(newValue)放在字段的内存地址上,如果CAS失败就重新(1),(2)步骤,一直到CAS成功,这种重复俗称CAS自旋.
伪代码:
do{
获得字段的期望值(oldValue)
计算出需要替换的新值(newValue)
} while(!(CAS(内存地址,oldValue,newValue))
当CAS 将内存地址的值与预期值进行比较时,如果相等,就证明内存地址的值没有被修改,可以替换成新值,然后继续往下运行,如果不相等,就说明内存地址的值已经被修改,放弃替换操作,然后重新自旋.
当并发修改的线程少,冲突出现的机会少时,自旋的次数也会很少,CAS的性能会很高,当并发修改的线程多,冲突出现的机会多时,自旋的次数也会很多,CAS的性能会大大降低. 所以,提升CAS无锁编程效率的关键在于减少冲突的机会
JNI的具体实现
Unsafe中的原子性方法有三个,如下:
/**
* Atomically update Java variable to <tt>x</tt> if it is currently
* holding <tt>expected</tt>.
* @return <tt>true</tt> if successful
*/
public final native boolean compareAndSwapObject(Object o, long offset,
Object expected,
Object x);
/**
* Atomically update Java variable to <tt>x</tt> if it is currently
* holding <tt>expected</tt>.
* @return <tt>true</tt> if successful
*/
public final native boolean compareAndSwapInt(Object o, long offset,
int expected,
int x);
/**
* Atomically update Java variable to <tt>x</tt> if it is currently
* holding <tt>expected</tt>.
* @return <tt>true</tt> if successful
*/
public final native boolean compareAndSwapLong(Object o, long offset,
long expected,
long x);
在剖析原子性方法的实现前,我们先来看看原子类委托方法,代码位于oop.hpp,文件路径为Hotspot\src\share\vm\oops。使用oopDesc::
public:
static oop atomic_compare_exchange_oop(oop exchange_value,
volatile HeapWord *dest,
oop compare_value,
bool prebarrier = false);
在oop.inline.cpp中提供了实现,文件路径为hostspot/src/share/vm/oops
inline oop oopDesc::atomic_compare_exchange_oop(oop exchange_value,
volatile HeapWord *dest,
oop compare_value,
bool prebarrier) {
if (UseCompressedOops) {
if (prebarrier) {
update_barrier_set_pre((narrowOop*)dest, exchange_value);
}
// encode exchange and compare value from oop to T
narrowOop val = encode_heap_oop(exchange_value);
narrowOop cmp = encode_heap_oop(compare_value);
narrowOop old = (narrowOop) Atomic::cmpxchg(val, (narrowOop*)dest, cmp);
// decode old from T to oop
return decode_heap_oop(old);
} else {
if (prebarrier) {
update_barrier_set_pre((oop*)dest, exchange_value);
}
return (oop)Atomic::cmpxchg_ptr(exchange_value, (oop*)dest, compare_value);
}
}
从代码可知,最终的代码实现委托给了Atomic::cmpxchg或Atomic::cmpxchg_ptr,所以最终的实现由Atomic类确定。
由于java的原子类是由Unsafe.java类实现的,我们就以CompareAndSwapInt方法为例是如何实现原子类的。文件路径为Hotspot\src\share\vm\prims\unsafe.cpp
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
//将java对象转化为JVM的oop(ordinary object pointer,普通对象指针)
oop p = JNIHandles::resolve(obj);
//将oop对象 转换为堆地址,通过oop对象和地址偏移量确定
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
//重点看Atomic::cmpxchg
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END
核心方法是Atomic::cmpxchg,这个方法所属的类文件是在OS_CPU目录下,不同平台的实现不同,与CPU操作有关,先来看下linux平台下Atomic::cmpxchg方法的实现,文件路径为hotspot\src\os_cpu\liunx_x86\vm\atomic_linux_x86.inline.hpp
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
int mp = os::is_MP();
//cmpxchgq %1,(%3)为要执行的指令,指令之间使用双引号,本方法中使用的比较并交换指令,并需要操作数1和操作数3(%1和%3) 输出:
// 第一个冒号后定义输出,
//=表示输出,a表示累加器a,"=a" (exchange_value)表示交换指令执行后,累加器a的值会赋给变量
//第二个冒号后面定义输入,
//"r" (exchange_value)表示将exchange_value的值存在寄存器中,并由指令中的%1引用,
//"a" (compare_value)表示将compare_value放在累加器a中,
//"r" (dest)表示dest指针值放到寄存器中,由指令中的%3引用,
//"r" (mp)表示mp放入寄存器中,由%4引用。
// 第三个冒号是寄存器列表修饰
//cc表示可以修改条件码寄存器,
//memory表示指令以不可预测的方式修改了内存。
__asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
: "=a" (exchange_value)
: "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
: "cc", "memory");
//输出结果
累加器的值(即compare_value的值),dest指针不变,只是指针指向的内存单元的数据变成了exchange_value,
// 所以,exchange_value和dest指向的内存的内容做了交换,cmpxchg函数最后返回compare_value的值
return exchange_value;
}
#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "
asm volatile表示linux下内嵌汇编(也兼容ANSIC),并且使用关键字禁止编译器指令重排序优化,而dest使用volatile修饰(禁止编译器优化存取,比如,为了存取速度,将dest的内容存储在寄存器中,当外部程序修改dest指向的内存单元时,寄存器感知不到,还是原来的值,从而导致程序错误),表示dest指向的值随时有可能被别的程序修改,所以每次使用的时候需要重新读取该变量,根据linux下嵌入汇编的规则,
再看看window平台的 Atomic::cmpxchg 方法实现,文件路径 hotspot\src\os_cpu\windows_x86\vm\atomic_windows_x86.inline.hpp
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
// alternative for InterlockedCompareExchange
int mp = os::is_MP();
__asm {
mov edx, dest //表示把目标地址放入edx寄存器中
mov ecx, exchange_value //把exchange_value值放入寄存器ecx中。
mov eax, compare_value // 需要和目标地址指向的值进行比较的值放入寄存器eax中。
LOCK_IF_MP(mp)
cmpxchg dword ptr [edx], ecx //执行比较操作并交换,这是一个原子操作
}
}
#define LOCK_IF_MP(mp) __asm cmp mp, 0 \
__asm je L0 \
__asm _emit 0xF0 \
__asm L0:
其中,dword ptr [edx]表示取目标地址的指向的值,由于edx的值只是一个指针,指针指向的存储单元的值和ecx宽度可能不同,所以需要明确指出dword上述汇编代码执行后,dest指向的内存中的值为exchange_value。
从liunx和window平台的实现中可知,都调用了LOCK_IF_MP,LOCK_IF_MP是根据当前处理器的类型来决定是否为cmpxchg指令添加lock前缀。MP是指multiple processor,即多核处理器。如果程序是在多处理器上运行,就为cmpxchg指令加上lock前缀(lock cmpxchg)。反之,如果程序是在单处理器上运行,就省略lock前缀(单处理器自身会维护单处理器内的顺序一致性,不需要lock前缀提供的内存屏障效果)。
总结,CAS 原子性实现是利用CPU的比较并交换指令(cmpxchg),不同的平台由内嵌汇编语法不同导致不同的实现,但是在Java调用时,不同平台最终达到的效果是一致的。