Synchronized的原理(汇编层)

原理描述:

通过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了

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值