Skip to main content
  1. posts/

彻底搞懂synchonized

·1224 words·3 mins

CPU执行过程

我们都知道Synchonized可以保证原子性,可以保证线程的同步。那么Synchonized是怎么保证的呢。 从CPU说起,

这是CPU的处理流程图,而线程之间无法同步的本质,就是数据的副本在多个CPU上操作,导致数据的不一致。那么要怎么解决这个问题呢?之前讲过volatile是在总线上通过MESI协议来保证对单个数据的读/写的原子性。但是无法解决i++这种读-改-写的复合操作。那么Synchonized是怎么做的呢?

监控锁(原子性保证)

使用Synchonized关键字封装的代码块在编译成汇编语言时,会在这段代码块之前加上monitorenter关键字,之后加上monitorexit关键字。加上这两个关键字怎么就可以保证数据的同步呢?

总线锁定

当cpu执行到monitorenter时,会在总线上发出lock信号,其他cpu收到lock信号之后,就不能操作缓存和内存中的值了。所以这个代价挺高的。

缓存锁定

总线锁定封锁了缓存和主存数据的读取和修改,为了降低总线锁定的代价,有些cpu把单个值的更新优化成了缓存锁定。对于下图的场景,当CPU1修改了缓存中的值时,CPU2修改时就发现缓存锁定了,无法修改了。

image.png
但是缓存锁定有两个限制:

  1. 只能针对单个缓存行的更改【缓存行:缓存的最小单位】
  2. 有些处理器不支持

Synchronized优化

虽然上诉过程可以在底层保证操作的原子性。但是总线锁定的成本也是很高的。而且JVM团队在实际场景中发现,出现并发的场景其实很少。 于是在JDK1.6版本就优化Synchronized的实现。主要包括锁消除,锁膨胀和锁升级的过程。

锁消除

在 synchronized 修饰的代码中,如果不存在操作临界资源的情况,会触发锁消除,你即便写了 synchronized ,他也不会触发

public synchronized void method(){
    // 没有操作临界资源
    // 此时这个方法的synchronized你可以认为没有
}

锁膨胀

如果一个循环中,频繁的获取和释放资源,这样的消耗很大。锁膨胀就会将锁的范围扩大,避免频繁的竞争和获取锁资源带来不必要的消耗。

public void method(){
    for(int i = 0;i < 999999;i++){
        synchronized(对象){

        }
    }
    
    // 这是上面的代码会触发锁膨胀
    synchronized(对象){
        for(int i = 0;i < 999999;i++){

        }
    }
}

锁升级

锁状态

  1. 无锁状态
  2. 偏向锁状态
  3. 轻量级锁状态
  4. 重量级锁状态

锁升级过程

image.png

Sychronized实现原理

锁升级过程

重量级锁实现

我们知道字节码层面,synchonized关键字会增加monitorenter和monitorexit,在在jvm层面是如何实现锁的处理的呢?主要是通过ObjectMonitor来实现操作的。

ObjectMonitor处理流程
  1. 我们线程刚进来时,会进入 Cxq 的队列中
  2. 当我们的 owner 释放锁时,会将 Xcq 里面的线程放到 EntryList 中
  3. 这个时候由 OnDeck Thread 去进行锁竞争,竞争失败的则继续留在 EntryList 中
  4. 当调用 Object.wait() 会进入 _WaitSet 队列,只要被唤醒时,才会重新进入 EntryList 中

在重量级锁中没有竞争到锁的对象会 park 被挂起,退出同步块时 unpark 唤醒后续线程。唤醒操作涉及到操作系统调度会有额外的开销。

image.png

参考

https://xiaohuang.blog.csdn.net/article/details/129848342?spm=1001.2014.3001.5502 https://xiaomi-info.github.io/2020/03/24/synchronized/