Java内存模型——volatile关键字

  最近工作中又用到了volatile关键字,一直以来就是单纯的使用,也没有仔细看过相关内容,这次借机会详细的整理了下有关volatile的资料,记录在案以备查阅。

  首先我们来看一个小例子:

 1 public class VolatileDemo1 {
 2     private boolean flag = true;
 3 
 4     public static void main(String[] args) throws InterruptedException {
 5         VolatileDemo1 demo = new VolatileDemo1();
 6         Thread thread = new Thread(() -> {
 7             long start = System.currentTimeMillis();
 8             while (demo.flag) {
 9             }
10             long end = System.currentTimeMillis();
11             System.out.println("终止了while循环!flag的值为:" + demo.flag);
12             System.out.println("耗时:" + ( end -start ));
13         });
14         thread.start();
15         TimeUnit.SECONDS.sleep(2);
16         demo.flag = false;
17     }
18 }

  这段代码是volatile关键字的典型应用场景之一,两个线程(主线程与thread 线程)通过共享一个变量进行信息交互,在上一段代码中,由于没有为flag变量加上volatile关键字,可以预见,线程thread中的while循环并不会跳出。那么,是不是只有加volatile关键字可以解决这个问题呢?或者说我们能不能不改动代码就达到目的(主线程中改变flag的值后,thread线程可以读到,使while循环可以跳出)。答案当然是可以的,我们可以采用以下方式:(如图)

  在虚拟机参数选项上加上-Xint(请注意,这个参数在JDK1.8版本及以上),同样能够使while循环跳出,那么这个-Xint参数到底有什么作用呢?请看以下截图:

  这张截图来自Oracle的Java HotSpot VM Options 官方文档,翻译过来的意思是:“以纯解释模式运行应用程序。禁用编译到本机代码,所有字节码由解释器执行。在这种模式下,just in time (JIT)编译器所提供的性能优势并不存在。”这么说只要是禁止了JIT即时编译,就起到了和加volatile一样的作用,那他们两个有什么区别吗?JIT即时编译又做了什么呢?请继续往下看。

  要扯明白上面的问题,我们还要说下Java的内存模型,首先什么是内存模型呢?周所周知,在现代计算机硬件系统的不断改进中,CPU和内存之间的多级缓存机制导致的缓存一致性问题,以及为了高效执行代码而进行的处理器优化和指令重排序问题,是并发编程中的可见性、原子性、有序性问题的硬件层面原因。在并发编程中,为了保证共享内存的正确性(可见性、有序性、原子性),内存模型定义了共享内存系统中多线程程序读写操作行为的规范。通过这些规则来规范对内存的读写操作,从而保证指令执行的正确性。它与处理器有关、与缓存有关、与并发有关、与编译器也有关。他解决了CPU多级缓存、处理器优化、指令重排等导致的内存访问问题,保证了并发场景下的一致性、原子性和有序性。

  说的直白点,内存模型就是解决多线程场景下并发问题的一个重要规范,而不同的编程语言对于这个规范,在实现上可能有所不同,而Java内存模型(Java Memory Model ,JMM)就是Java编程语言提供的一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。同时Java中提供了一系列和并发处理相关的关键字,比如volatilesynchronized等,其实这些关键字就是对Java内存模型规范的一种实现,他们封装了Java内存模型规范底层的实现后提供给程序员使用,用来解决Java并发编程问题。

  有了上面的对于内存模型的描述,那么我们就很好理解了,如果要解决并发编程中的问题,最简单直接的做法就是不使用处理器执行代码的优化技术、不使用指令重排序、不使用CPU缓存等等优化技术。但是,这么做显然就是因噎废食了。而我们使用的-Xint参数,根据官网的的描述(没有JIT编译的部分,全部是由解释器解释执行)和最终的执行结果看,我们可以推论出:-Xint参数的作用在废止JIT即时编译应该也就是废除了指令重排序和CPU缓存等优化技术,这里我没有深入的研究过这个参数和JIT,只是临时应用,所以做出的推论完全是根据官网的的描述和代码的执行结果看,不一定完全正确,如果有了解的大神,还请不吝赐教。

  那下面我们就来看看volatile关键字,显然volatile关键字不会和-Xint一样因噎废食,全面封杀优化技术,那他是怎么做的呢?

  首先内存模型解决并发问题主要采用两种方式:限制处理器优化和使用内存屏障,volatile作为内存模型规范的一种应用实现方式,自然也是实现这两种方式。

  对于volatile变量,生成的汇编代码在volatile修饰的共享变量进行写操作的时候会多出一个Lock前缀的指令,将这个缓存中的变量回写到系统主存中。

  lock前缀指令实际上相当于一个内存屏障(也称内存栅栏),内存屏障会提供3个功能:

  1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

  2)它会强制将对缓存的修改操作立即写入主存;

  3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

  功能1相当于禁止指令重排序优化,解决了并发变成中有序性问题。

  功能2和3,由于他处理器的缓存遵守了缓存一致性协议,也会把这个变量的值从主存加载到自己的缓存中。这就保证了一个volatile变量在并发编程中,其值在多个缓存中是可见的,解决了并发变成中可见性问题。

  注意:volatile并不能解决原子性问题。

  通过以上一波操作,volatile完成了并发编程时解决有序性和可见性问题。

 补充内容:

 Java虚拟机有3种执行方式,分别是解释执行、混合模式和编译执行,默认情况下处于混合模式中

编译:字节码 --- jit提前编译 -- 汇编

解释:字节码 – 一段段编译 – 汇编

混合 :– 运行的过程中,JIT编译器生效,针对热点代码进行优化

内存屏障参考资料:

https://blog.csdn.net/dd864140130/article/details/56494925

参考资料:

http://www.uucode.net/201504/jvm5

https://docs.oracle.com/javase/8/docs/technotes/tools/windows/java.html

猜你喜欢

转载自www.cnblogs.com/peripateticism/p/11065864.html