JMM怎么解决原子性,可见性,有序性问题
java提供了一系列的和并发处理的关键字,比如: valatile,final,syncronized, juc,这些java内存模型封装了底层的实现后提供给开发人员的使用的关键字.在开发过程中我们可以直接是用关键字syncronized等关键字.使我们不用关注底层的编译器的优化,缓存一致性问题,所以在java内存模型中,除了定义一套规范外,还提供了开发指令的底层进行封装,给开发人员使用.
原子保障
在java中提供两个指令 moniterenter和moniterexit,在java中 对应的syncroinzied进行对应 原子操作.
可见性
java中通过volatile关键字提供了一个功能,那就是被其修饰的变量被修改后可以立即同步到主内存中,被其修饰的变量每次使用前都是从主内存刷新,因此volatile可以保证多线的变量的可见性.除了volatile,final ,syncronized也是可以保证可见性的.
有序性
在java中,可以用syncronized和volatile来保证多线之间的操作的有序性.实现方式有区别,volatile防止指令重排,syncronzied是同一时刻保证一个线程执行.
volatile是如何保证可见性的
volatile变量修饰的共享变量,进行写操作的时候会多出一个lock前缀汇编指令,会触发总站所,缓存锁,通过缓存一致性协议解决可见性问题
对于声明了volatile的变量进行操作写操作.JVM就会向处理器发送一条lock前缀指令,把这个缓存行数回写到内存,在根据我们的MESI缓存一致性协议,来保证多CPU下高速缓存中的数据的一致性.
volatile防止指令重排
指令重排的原因就是为了最大化提高CPU的利用效率及性能,CPU乱序执行优化在单核时代并不影响正确性,但是在多核时代的多线程能够在不同的核心上实现真正的并行,一旦线程之间数据共享,可能出现一些不可预料的问题.
指令重排必须要遵循的原则是,不影响代码的执行结果.编译器和处理器不会改变存在数据的依赖关系的两个操作的执行顺序.
这个语义,实际上是as-if-serial语义,不管怎么重排序,单线程程序的执行结果不会改变,编译器,处理器必须尊徐as-if-serial.
多核心多线程下的指令重排影响
private static int x=0,y=0;
private static int a=0,b=0;
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
a=1;
x=b;
});
Thread t2=new Thread(()->{
b=1;
y=a;
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("x="+x+"->="+y);
如果不考虑编译器重排序和缓存可见性,上面会导致 x=0,y=1;x=1,y=0;y=1,因为可能先执行t1/t2,也可能是反过来,还可能是t2/t1交替执行,但是这段代码执行的结果可能是事x=0,y=0 ,这就是乱序执行的情况下导致结果,因为线程t1内部的两行代码之间不存在依赖,因此可以bx=b乱序到a=1之前;同时线程t2中的y=a也可以早于t1=中的a=1执行,那么它们的执行顺序可能是
t1:x=b;
t2:b=1;
t2:y=a;
t1:a=1;
从上面的例子看,重排序会导致可见性问题,但是重排序带来的问题远远大于可见性,因为并不是简简单单的指令读和写,比如DCL部分的初始化问题,单纯的解决可见性问题还不够,还需要解决处理器重排序的问题.
内存屏障
内存屏障需要解决我们前面提到的两个问题,一个是编译器的乱序和CPU执行的乱序.我们可以分为优化屏障和内存屏障这两个机制解决.
看一些CPU 层面上的内存屏障
CPU的乱序执行,本质还是,由于多CPU的机器上,每个CPU都存cache,当一个特定数据第一次被特定的CPU获取时,由于该CPU的缓存不存在,就会从内存去获取.被加载到CPU高速缓存中后能够快速访问,当某个CPU进行操作时,他必须确保其他的CPU已经将这个数据从他们的缓存中移除,这样才可以让CPU安全的修改数据,显然,存在多个cache时,我们必须通过一个cache一致性协议来避免数据不一致问题,而这个通讯过程可能导致乱序访问的问题,也就是运行内存乱序访问
现在的CPU架构都是提供内存屏障功能,在x86的cpu中,实现了相对应的内存屏障,写屏障,读屏障,和全屏障,主要作用是
- 防止指令之间的重排序
- 保证数据的可见性
store barrier
store barrier 称为写屏障,相当于storestore barrier,强制所有的storestore内存屏障之前的所有执行,都要在该内存屏障之前执行,并且发出缓存失效,所有在store barrier 指令之后的store指令,都必须在storestore barrier屏障之前的指令执行完成后再执行,也就是禁止了屏障前后的指令重排序,使所有的store barrier之前发生的内存更新都是可见的(这里是指的修改值可见的操作也是可见的)
load barrier
load barrier 称为读屏障,相当于loadload barrier,强制所有在load barrier读屏障之后的load指令,都在load barrier屏障之后执行,也就是禁止对load barrier读屏障前后的load指令进行重排序,配合store barrier 使得所有 store barrier之前发生的 内存更新,对load barrier之后的操作是可见的
full barrier
full barrier 成为全屏障,相当于storeload 是一个全屏障,因为它同时具备前两种屏障效果.强制了所有的storeload barrier之前的store/load指令,都在该屏障之前被执行,所有在该屏障之后的store/load指令,都在该屏障之后执行,禁止对store/load屏障的前后的指令进行重排序
总结:内存屏障解决只是解决顺序一致性问题,不解决缓存一致性问题.缓存一致性是cpu缓存锁及MESI协议来解决的,缓存一致性协议只关心缓存一致性问题,只是在编译器上如何解决指令重排问题.
在编译器层面,通过volatile关键字,取消编译器的缓存和重排序.保证了在优化屏障屏障之前指令不会再优化屏障之后执行.这样就保证在编译期间优化不会影响到实际代码逻辑顺序
JMM中把内存屏障分为4类,通过不同的语义下使用,不同的内存屏障来进行特定的类型的处理重排序
LoadLoad Barriers ,load1;loadload;load2,确保load1的数据优先于load2及所有后续的装载指令的装载.
StoreStore Barriers,store;storestore;store2; 确保store1数据对其他处理器可见性优于store2及所有后续存储指令的存储
LoadStore Barriers,load1;loadstore;store2确保load1装载优先于store2及后续的存储指令刷新到内存
StoreLoad Barriers; store1;StoreLoad;load2;确保store1数据对其他都是处理器是可见,优先于load2及所有的后续装载指令的装载
volatile为啥不能保证原子性
- 读取volatile变量的值到local
- 增加变量的值
- 把local的值写回让其他线程可见