synchronized 深入剖析
synchronized 是可以保证可见性的
先介绍一下可见性是什么东西:
线程要修改一个变量,这个变量是在主内存中存储的,线程修改时,要先去主内存读取一份到自己的工作内存中,这个工作内存是线程私有的,其他线程看不到,因此如果当前线程修改完毕,没有及时刷新到主内存,或者其他线程读取的时候,没有及时去主内存中读取最新值,就会导致出现数据的不一致问题,也就是数据的不可见,那么保证数据的可见性,就是要保证多个线程中的数据一致性,避免其中一个线程修改变量之后,其他线程看不见变量的更新!
接下来说一下可见性的保证:
先从底层说起,可见性的保证,在底层其实是通过 MESI 协议来保证的,也就是保证多个处理器(CPU)和主内存之间的数据一致性,从而保证在操作系统层面上,多个线程之间对数据的更新是可见的
这里说一下 MESI 如何保证数据一致性,只简单说一下,毕竟我们不是专攻底层的
在 MESI 协议中,主要有 两个关键机制
来保证数据的一致性:flush 和 refresh
- flush
将自己更新的值刷新到高速缓存里去,让其他处理器在后续可以通过一些机制从自己的高速缓存里读到更新后的值
并且还会给其他处理器发送一个 flush 消息,让其他处理器将对应的缓存行标记为无效,确保其他处理器不会读到这个变量的过时版本
- refresh
处理器中的线程在读取一个变量的值的时候,如果发现其他处理器的线程更新了变量的值,必须从其他处理器的高速缓存(或者是主内存)里,读取这个最新的值,更新到自己的高速缓存中
上边是硬件级别保证可见性的原理,那么在上层保证可见性其实是基于内存屏障来做的,内存屏障的作用可以理解为强制去读取最新值以及将最新值刷回主内存,也就是有内存屏障的地方会强制线程去执行 refresh 和 flush 动作,从而保证数据的一致性
synchronized 保证可见性:
那么 synchronized 保证可见性其实也就是通过 内存屏障
来保证的,在进入 synchronized 代码块和退出的时候,都会插入内存屏障,目的就是保证在进入的时候,强制去执行 refresh 操作,这样可以保证读取到变量的最新值,而在退出 synchronized 代码块时,也就是对变量的修改已经完成了,此使强制去执行 flush 操作,可以保证将变量的最新值给刷新到主内存中去
如下,在 synchronized 中添加的内存屏障:
int b = 0;
int c = 0;
synchronized (this) { --> monitorenter
--> Load 内存屏障
--> Acquire 内存屏障
int a = b;
c = 1;
--> Release 内存屏障
} --> monitorexit
--> Store 内存屏障
Acquire 屏障 = LoadLoad + LoadStore
- Acquire 屏障确保一个线程在执行到屏障之后的内存操作之前,能看到其他线程在屏障之前的所有内存操作的结果
Release 屏障 = LoadStore + StoreStore
- Release 屏障用于确保一个线程在执行到屏障之后的内存操作之前,其他线程能看到该线程在屏障之前的所有内存操作的结果
这里再介绍一下 JVM 中的内存屏障,也不用都背会,了解内存屏障这个东西就可以了,背会其实没有意义
JMM 中有 4 类内存屏障
:(Load 操作是从主内存加载数据,Store 操作是将数据刷新到主内存)
LoadLoad
:确保该内存屏障前的 Load 操作先于屏障后的所有 Load 操作。对于屏障前后的 Store 操作并无影响屏障类型StoreStore
:确保该内存屏障前的 Store 操作先于屏障后的所有 Store 操作。对于屏障前后的Load操作并无影响LoadStore
:确保屏障指令之前的所有Load操作,先于屏障之后所有 Store 操作StoreLoad
:确保屏障之前的所有内存访问操作(包括Store和Load)完成之后,才执行屏障之后的内存访问操作。全能型屏障,会屏蔽屏障前后所有指令的重排