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了

 

<think>嗯,用户问的是Javasynchronized关键字如何保证原子性、有序性和可见性,需要了解实现原理。首先,我得回忆一下synchronized的基本机制。根据引用里的内容,synchronized通过monitorenter和monitorexit这两个字节码指令来实现锁的获取和释放。那原子性方面,引用4提到synchronized块之间的操作具备原子性,因为JVM的lock和unlock操作确保了代码块整体作为一个原子执行。 接下来是有序性。用户可能知道指令重排序会影响多线程的执行结果,而synchronized如何防止这个呢?我记得锁的互斥性会让线程串行执行,这样在同步块内的代码即使被重排序,也不会影响到其他线程看到的顺序,所以synchronized通过互斥来保证有序性,引用1也提到这点,把并发执行变成序列化。 然后是可见性。JMM中,当线程释放锁时,会把变量刷回主内存,获取锁时则会从主内存重新读取,确保共享变量的可见性。引用4提到基础数据类型的原子性由JVM保证,而synchronized通过lock/unlock提供更大范围的原子性,这也涉及到可见性。 可能用户还关心底实现,比如汇编的lock指令,引用1里提到lock cmpxchg,这个指令会锁定总线或缓存行,确保操作的原子性,同时内存屏障防止指令重排。这部分需要结合JVM和硬件面来解释。 另外,用户提供的引用中有同步方法和同步代码块的例子,说明不同使用方式锁的对象不同,比如类对象或实例对象,这可能影响到锁的范围,但核心原理应该是一样的。需要明确说明不同用法但原理相同。 还要注意,用户的问题可能涉及到对这三个性质的具体实现机制的区别,比如原子性是通过互斥,可见性是通过内存同步,有序性是通过禁止重排序。需要分点详细说明,确保回答结构清晰。 最后,生成的相关问题要围绕synchronized的实现细节、锁升级过程、与volatile的区别以及实际应用中的注意事项,这样能帮助用户深入理解。</think>### Javasynchronized关键字如何保证原子性、有序性和可见性? #### 1. **原子性** **定义**:原子性指一个操作是不可中断的,要么全部执行成功,要么完全不执行。 **实现原理**: - synchronized通过互斥锁(`monitorenter`和`monitorexit`字节码指令)保证同步代码块内的操作作为一个整体执行[^4]。 - 线程进入同步代码块前必须获取锁,其他线程无法同时执行该代码块,从而避免了竞态条件。例如: ```java public synchronized void increment() { count++; // 即使包含多个底操作(读-改-写),也能保证原子性 } ``` #### 2. **可见性** **定义**:一个线程修改共享变量后,其他线程能立即看到修改后的值。 **实现原理**: - 在释放锁时,JVM强制将工作内存中的变量刷新到主内存; - 在获取锁时,JVM会清空本地内存,从主内存重新加载变量[^4]。 - 通过内存屏障(Memory Barrier)禁止指令重排序,确保共享变量的修改对所有线程可见[^1]。 #### 3. **有序性** **定义**:程序执行的顺序符合代码的先后顺序。 **实现原理**: - synchronized通过互斥锁将并发执行变为串行执行,相当于隐式禁止了同步代码块内的指令重排序[^1]。 - 例如以下代码中,`doSth1()`和`doSth2()`内部的代码不会被重排序到同步块外: ```java public void doSth1() { synchronized (this) { // 代码块内的操作不会被重排序到锁外 } } ``` --- ### 底实现细节 1. **锁的升级过程**: synchronizedJVM中经历了从偏向锁→轻量级锁→重量级锁的升级过程,以适应不同竞争场景[^1][^3]。 2. **汇编实现**: 最终通过`lock cmpxchg`(CAS指令)实现原子操作,该指令会锁定总线或缓存行,确保原子性和可见性[^1][^4]。 --- ### 示例代码分析 ```java public class SynchronizedTest { // 同步代码块(锁类对象) public void doSth1() { synchronized (SynchronizedTest.class) { // monitorenter System.out.println("HelloWorld"); } // monitorexit } // 同步方法(锁当前实例) public synchronized void doSth2() { // 方法标记ACC_SYNCHRONIZED System.out.println("HelloWorld"); } } ``` - 同步代码块:通过`monitorenter`和`monitorexit`显式控制锁的获取和释放[^2][^4]。 - 同步方法:通过方法访问标志`ACC_SYNCHRONIZED`隐式实现锁机制[^3]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值