Java 中synchronized关键字及volatile的可见性实现
JMM(JAVA内存模型)
JMM工作原理如上图所示,一些被定义的变量都存放在主内存中,当一个线程想要修改一个变量的值时,那么这个变量会从主内存中拷贝到线程的工作内存(CPU缓存)中。之后线程对变量值做了更改,又会重新拷贝回主内存中。大家通过描述也可以看出来这些操作是分步执行的,这样就无法保证可见性和原子性。对于这种情况java也给出了很多解决办法,其中就有synchronized以及volatile
synchronized
JMM对于synchronized有两条规定:
-
线程解锁前,必须把共享变量的最新之刷新到主内存中
-
线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(加锁与解锁需要时同一把锁)
这样就会导致synchronized代码块是按照下面的顺序执行的:
-
获得互斥锁,清空工作内存并从主内存拷贝变量的最新副本到工作内存
-
执行代码
-
将更改后的共享变量的值刷新到主内存
-
释放互斥锁
正是上面的执行顺序使得synchronized具备内存可见性。显示锁(Lock)和synchronized有相同的内存可见性语义,其实原理跟synchronized类似。
volatile
为了实现 volatile 的可见性内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
下面是基于保守策略的 JMM 内存屏障插入策略:
-
在每个 volatile 写操作的前面插入一个 StoreStore 屏障。
-
在每个 volatile 写操作的后面插入一个 StoreLoad 屏障。
-
在每个 volatile 读操作的后面插入一个 LoadLoad 屏障。
-
在每个 volatile 读操作的后面插入一个 LoadStore 屏障。
下面是保守策略下,volatile 写操作 插入内存屏障后生成的指令序列示意图:
下面是在保守策略下,volatile 读操作 插入内存屏障后生成的指令序列示意图:
上述 volatile 写操作和 volatile 读操作的内存屏障插入策略非常保守。在实际执行时,只要不改变 volatile 写-读的内存语义,编译器可以根据具体情况省略不必要的屏障。
对于volatile是如何实现内存可见性,深入来说:是通过加入内存屏障和禁止重排序优化来实现的。(重排序指单线程中在保证执行结果不变的前提下java虚拟机为了提升处理速度可能会将指令重排,达到最合理化)
对volatile变量执行写操作时,会在写操作后加入一条store屏障指令,执行效果:
-
改变线程工作内存中的volatile变量副本的值
-
将改变后的副本的值从工作内存刷新到主内存
对volatile变量执行读操作时,会在读操作前加入一条load屏障指令,执行效果:
- 从主内存中读取volatile变量的最新值到线程的工作内存中
- 从工作内存中读取volatile变量的副本
简单来说:volatile变量在每次被线程访问时,都强迫从sy主内存中重读变量的值,而当该变量发生变化时,又会强迫线程将最新的值刷新到主内存。这样在任何时刻,不同的线程总能看到该变量的最新值。从而保证了变量的内存可见性。
synchronized和volatile的比较
-
volatile不需要加锁,比synchronized更加轻量级,不会阻塞线程
-
从内存可见性讲,volatile读相当于加锁,volatile写相当于解锁
-
synchronized既能保证可见性,又能保证原子性,而volatile只能保证可见性,无法保证原子性