首先贴下jdk8里面java的native方法的声明,在文件 jdk.internal.misc.Unsafe.java 里面:
/**
* Atomically updates Java variable to {@code x} if it is currently
* holding {@code expected}.
*
*
This operation has memory semantics of a {@code volatile} read
* and write. Corresponds to C11 atomic_compare_exchange_strong.
*
* @return {@code true} if successful
*/
@HotSpotIntrinsicCandidate
public final native boolean compareAndSetInt(Object o, long offset,
int expected,
int x);
这个方法原子性的更新变量的值为x如果原来的值是期待的expected的值时。
jdk8里面native(c++)代码的实现,在文件 src/share/vm/prims/unsafe.cpp:
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSetInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x)) {
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
其中UNSAFE_ENTRY和UNSAFE_END是定义的两个宏,暂时不做分析
上面的方法主要有三个逻辑:
oop p = JNIHandles::resolve(obj);//获取obj这个对象在jvm里面的对应的内存对象实例
jint* addr = (jint *)index_oop_from_field_offset_long(p, offset);//通过上面的内存对象实例结合传入的偏移量获取字段对应内存对象字段在对象里面的偏移的地址,可以看到是一个int的指针。
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;//通过调用原子方法的结果判断返回值是不是原来的e值,如果是表示更新成功,否则更新失败。
下面分析Atomic::cmpxchg(x, addr, e)方法的具体实现:
这个方法在atomic_linux_x86.hpp文件里面,不同的系统不同的cpu架构对应不同的实现代码及文件,这里以linux x86架构分析。
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value, cmpxchg_memory_order order) {
int mp = os::is_MP();
__asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
: "=a" (exchange_value)
: "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
: "cc", "memory");
return exchange_value;
}
方法主要以内联汇编的形式组织代码,并且加了volatile防止编译器gcc进行指令优化,保证编写的汇编代码的执行顺序于编写的顺序一致。关于c++内联汇编可以参考:GCC内联汇编基础
现在分析下这个汇编代码的逻辑:
首先分析下汇编指令的操作码cmpxchgl,指令cmpxchg后面加了一个后缀l在at&t的汇编风格表示操作的操作数的字长为双子即32位,和l类似的是b和w,分别表示字节(8位)和字(16位)。
汇编指令格式,at&t风格: 操作码 源操作数 目的操作数。
指令的作用是:CMPXCHG r,r/m 。 将累加器AL/AX/EAX/RAX中的值与目的操作数比较,如果相等,源操作数的值装载到目的操作数,zf置1。如果不等,目的操作数的值装载到AL/AX/EAX/RAX并将zf清0。
结合这里的例子,可以看到:%1是源操作数,(%3)是首操作数,compare_value的值是被比较的值,通过"a" (compare_value)放入寄存器eax,
cmpxchgl %1,(%3)比较第三个变量(%3)这个是at&t的风格类似intel风格[%3],这里表示内存为dest偏移量的地址的值。指令比较的时候:首先把compare_value赋值给eax,然后比较eax和(%3)里面的值,如果(%3)的值和eax相等,就把%1的值装载到(%3)里面,这时(%3)里面的值为exchange_value,如果不等就把(%3)里面的值装载到寄存器eax里,同时把寄存器eax的输出到绑定的exchange_value变量上。这个方法最后返回的是exchange_value。结合前面调用的方法可以看到如果成功更新,则变量exchange_value的值是之前内存的值,如果更新失败则是原来的值。
再分析一下代码里面的前两行:int mp = os::is_MP(); LOCK_IF_MP(%4) ;
第一行就是判断当前系统是否是多核的,第二行代码通过前面的变量mp(%4就是mp)判断是否是多核,如果是就加上lock指令前缀。关于locklock指令前缀:
在多处理器环境中,LOCK#信号可确保在断言该信号时处理器拥有对任何共享内存的独占使用。
总结:
首先是通过lock保证操作的内存行具有cpu独占性;
通过cmpxchg命令来进行原子比较交换操作;
在c++里面通过内联汇编的方式同时加上volatile让gcc不进行编译优化保证汇编代码执行的顺序。
综上,通过以上三点最终保证了在多核cpu上面cas执行的原子性。