原理描述:
通过C++层可以了解到其常用方法为CAS方法, 比如:
//通过CAS尝试把monitor的`_owner`字段设置为当前线程
cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ;
调用的Atomic类中的cmpxchg_ptr方法, 其实java中原子类Atomic包中的底层也是atomic.cpp这个类
这里从java里Atomic包中的Unsafe类的compareAndSwapInt()方法入手直到最后的cmpxchg指令
注: 从C++哪开始会比较晦涩难懂, 需要反复联系上下文查看代码才能理解
从java代码看起
// 从实际的代码案例看起, 分析值是如何变化的
AtomicInteger count = new AtomicInteger(10); //初始值为10
count.incrementAndGet(); //做自+1操作
// 跳转incrementAndGet
public final int incrementAndGet() {
// getAndAddInt可知返回值是10 然后+1返回
// this为本对象=10, offset为本对象初始内存地址到实际值存放位置的偏移量=12
// 第三个参数为增加的数量1
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
// 跳转getAndAddInt
public final int getAndAddInt(Object var1=10, long var2=12, int var4=1) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2); // 根据偏移量取出该对象实际值的存放变量
} while(!this.compareAndSwapInt(var1=10, var2=12, var5=10, var5 + var4=10+1=11)); //核心调用
// 从这里的返回看出compareAndSwapInt返回的要么成功要么失败, 不会返回修改后的值, 所以java代码中getAndAddInt的返回结果是这里var5+1的得值
return var5;
}
// 跳转compareAndSwapInt
public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
// 由native修饰表明由C++实现, 再次说明参数含义:
// o为本对象, offset为本对象初始内存地址到实际值存放位置的偏移量
// expected要修改为的值, x为比较值(与o比较)
进入C++代码
Unsafe类里的compareAndSwapInt源码如下:
本源码在OpenJDK8里的路径为: openjdk/hotspot/src/share/vm/prims/unsafe.cpp
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj=10, jlong offset=12, jint e=10, jint x=10+1=11))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandLes::resolve(obj);//取出数据指针,这个指针是指向oop(即ordinary object pointer普通对象指针)
jint* addr = (jint *) index_oop_from_field_offset_Long(p, offset);//取出这实际数据实际在内存中的指向地址
return (jint) (Atomic::cmpxchg(x=11, addr=obj的指针[10], e=10)) = e; // 核心调用,当结果=e也就是=10时修改成功
// 为什么要做=? 因为内部最终赋值成功后obj的值
UNSAFE END
OpenJDK8的路径是这个: openjdk/hotspot/src/share/vm/runtime/atomic.cpp
//到了这个类中, 一切比较都使用byte数据判断了
jbyte Atomic::cmpxchg(jbyte exchange_value=11, volatile jbyte* dest=obj的指针[10]的byte数据, jbyte compare_value=10)
assert(sizeof(jbyte) == 1, "assumption."); // 断言byte大小为1
uintptr_t dest_addr = (uintptr t)dest;
uintptr_t offset = dest_addr % sizeof(jint);
volatile jint* dest_int = (volatile jint*)(dest_addr - offset);
jint cur = *dest_int; // cur=10
jbyte* cur_as_bytes = (jbyte*)(&cur); //指向cur的btye数据的指针
jint new_val = cur; //new_val=10
jbyte* new_val_as_bytes = (jbyte*)(&new_val); //指向new_val的btye数据的指针
new_val_as_bytes[offset] = exchange_value; //将将指向byte的数据覆盖为exchange_value的byte数据,也就是11的byte数据, 那么自然new_val也就变成了11
//cas的循环核心, obj的指针[10]=10时会循会一直循环, 直到obj的指针[10]变为obj的指针[11]则循环不再成立
while (cur_as_bytes[offset] == compare_value) { //持续判断, cur的byte数据
jint res = cmpxchg(new_val=11, dest_int=obj的指针[10], cur=10); //核心的赋值代码
if (res == cur) break; //res=cur时表示赋值成功,跳出循环
// 失败时说明有竞争, 其他线程先行改变了obj在内存里的值, 则需要继续重试
cur = res; //重试需要将cur更新为最新值, cur=res=obj在内存中最新的值
new_val = cur;//new_val=10
new_val_as_bytes[offset] = exchange_value;//改为11
}
return cur_as_bytes[offset]; //循环一次成功是返回10的byte数据, 循环多次时说明发生了竞争, 每次竞争过后cur得值都会被赋值成obj最新在内存里的值, 那就不是10了.
//当然, 这里是存在ABA问题的, 不过这里不处理, ABA问题是由java层面上处理的.
}
以上循环的核心为cmpxchg方法, 该方法由在引入的包中
#include ”runtime/atomic.intine.hpp"
而atomic.inline.hpp里声明是会根据不同的操作系统引入不同的核心包
进入C++内联汇编
以其中的linux操作系统 x86处理器为例 atomic_linux_x86.inline.hpp
OpenJDK中路径如下: openjdk/hotspot/src/os_cpu/linux_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(); //判断是否是多处理器
// 这是C++内联汇编写法, 也就是汇编语言在C++中的内联汇编语法写法
__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; //这里返回的是寄存器eax的值(最后eax写入到了exchange_value中)
内联汇编的语法格式
想要看懂以上代码, 就得先了解内联汇编的语法格式
__asm__ volatile("Instruction List" //要执行的指令
: Output //要写出的值
: Input //要读取的值
: Clobber/Modify); //可能会对哪些寄存器或内存进行修改
__asm__表示汇编的开始, volatile表示禁止编译器优化
"cc"代表可能会修改标志寄存器(EFLAGS)
"memory"代表会修改内存
%1代表参数占位符, 下标从0开始, %1的表达式为"r"(exchange_value)
(%3)代表参数占位符, 下标从0开始,()代表实际值, (%3)表达式为"r"(dest)就是随机出来的寄存器的值
"=a" (exchange_value) 第0个参数, 等号(=)表示当前输出表达式的属性为只写, a表示寄存器EAX/AX/AL的简写.
"r"(exchange_value) 第1个参数, r表示寄存器.
"r"约束指明gcc使用任一可用的寄存器
LOCK_IF_MP(%4) 是个内联函数:
#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "
// 效果mp为0时会执行lock;指令,否则不会
// 解释: mp值判断是否添加lock前缀, 多核处理器需要, 单核不需要. mp值表示是否为多核处理器.
// 带有lock前缀的指令在执行期间会锁住总线,使得其它处理器暂时无法通过总线访问内存.
翻译汇编代码
根据以上规则手工将内联汇编翻译为汇编代码
此时先明确该方法中几个传入变量的值为多少:
exchange_value=11, dest=objde指针[10], compare_value=10, mp=1(当前为多核处理器)
// mov指令作用: mov 读取数据,数据写入位置
mov 11,ecx; //赋值exchange_value到ECX
mov 10,eax; //赋值compare_value到EAX
mov offset obj指针,edx; //赋值obj指针的地址(如00491000)到EDX
mov 1,ebx; //mp==1标示多核处理器
cmp $0,ebx; //比较ebx==0
je 1f; //如果成立跳转到标记为1:的位置
lock; // 多核处理器需要加lock指令保证下一句指令的内存操作被主线锁定,其他核不能操作
1: cmpxchg ecx, dword ptr ds:[edx]; //CPU级别的比较赋值
mov eax,$exchange_value; //最后将eax值写到exchange_value中,会用于判断是否复制成功
这里ecx,edx,ebx因为表达式是"r", 在实际运算中是随机选择可用的寄存器, 这里翻译是为了方便看, 实际并不固定. 只有eax, 读和写都用了表达式"a"指定了从哪读,写到哪. 要了解这个只需要了解到每个CPU核都有4个通用数据寄存器EAX,EBX,ECX,EDX即可, CPU还有其他类型寄存器, 但我认为这里不需要了解这么多. dword ptr ds:[edx]的意思为从edx中指向地址的内容作为操作值(这里的指向不一定是内存中的地址(可能是栈), 不过在当前代码中指向的就是内存的地址)
cmpxchg执行过后会由两种结果
EAX比较ECX
1.相等: obj指针指向的值从10变为11, eax=10(开始从compare_value读入的)
2.不相等: eax=obj指针指向的值, 因为只有obj指针指向的值已经不是10了才会与eax的10不相等
汇编最后会将eax的值写到exchange_value中
c++方法最后最后将exchange_value变量返回给调用者.
调用者代码为:(完整的看上边)
// 根据上述, 调用后返回的值赋给res
jint res = cmpxchg(new_val=11, dest_int=obj的指针[10], cur=10);
// 内部赋值成功,res=10,其实就是传进去cur的10, 自然比较相等就可以跳出循环了
// 内部赋值失败,res=obj在内存中最新的值, 自然是不等于cur的10的, 所以要重新循环(cas操作)
if (res == cur) break;
汇编指令cmpxchg
根据以上的汇编翻译其核心指令为cmpxchg
◆cmpxchg的作用: 比较并交换操作数
◆cmpxchg的语法: cmpxchg 操作数1, 操作数2
◆cmpxchg的逻辑:
固定用EAX和操作数2比较
相等: 操作数1 复制到 操作数2 中
不相等: 操作数2 复制到 EAX 中
套入以上代码的场景就是:
eax==ecx
相等: dword ptr [edx]=ecx, dest的指针指向的值变为了ecx的值
不相等: eax=dword ptr [edx]
写一个简单的demo
// 我的电脑是windows平台, 汇编指令的操作数顺序与liunx是相反的
MOV ECX,11 // 赋值11给ECX
MOV EAX,10 // 赋值10给EAX
MOV EDX,OFFSET 00491000 // 把指针赋值给EDX
CMPXCHG DWORD PTR DS:[EDX],ECX // 比较EDX指向的值是否与EAX相等
内存区域:
地址 十六进制转储
00491000 10 00 00 00
运算结果:
比较结论: EDX指向的值与EAX相等
寄存器:
ECX: 11
EAX: 10 // 若是不相等这里就会变成11
EDX: OFFSET 00491000
内存区域:
地址 十六进制转储
00491000 11 00 00 00 // 相等:这里变为11了