目录
Java的内存模型JMM(Java Memory Model)
为什么使用 volatile关键字呢?给大家举个例子吧!!!
很大的一个原因就是关于编译器自动优化的问题,看下面一段代码:
- class Counter{
- public static int count;
- }
- public class Test {
- public static void main(String[] args) {
-
- Thread t1 = new Thread(()-> {
- while(Counter.count==0) {
-
- }
- System.out.println("t1线程结束");
- });
- Thread t2 = new Thread(()-> {
- System.out.println("输入一个数:");
- Scanner scanner = new Scanner(System.in);
- Counter.count = scanner.nextInt();
-
- });
- t1.start();
- t2.start();
- }
- }
以上代码,当你执行时,你会发现你输入一个数,改变了count的值,但代码依旧没停止,还在while循环中执行着,出现这个问题的原因就是编译器的自动优化,到底是怎么优化的?
首先每进行while条件的比较时,都会读取内存中count的值,将其加载到CPU寄存器中,再进行计算比较处理。而在此时,编译器会以为没有人再去修改count的值,而读取内存加载到寄存器这又是相对比较大的开销(与计算比较处理相比),所以编译器在这里自动优化,省去了读取内存这一步骤,直接取寄存器的数据进行比较。
这里需要注意的是,编译器自动优化这件事,对于我们这些没有编写过JVM的程序员来说,算是一个未知的东西,例如上述代码我们在循环里面加上代码:
- try {
- Thread.sleep(500);
- } catch (InterruptedException e) {
- throw new RuntimeException(e);
- }
我们再次运行时,编译器又没有对其进行优化了
volatile是Java提供的一种轻量级的同步机制。Java 语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量,相比于synchronized(synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。但是volatile 变量的同步性较差(有时它更简单并且开销更低),而且其使用也更容易出错。
volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在JVM底层volatile是采用“内存屏障”来实现的。观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
关于这三个特性是什么意思,这篇博客里面有说明:http://t.csdn.cn/LN8qP
- //保证内存可见性
- class Counter{
- public static volatile int count;
- }
- public class Test1 {
- public static void main(String[] args) {
-
- Thread t1 = new Thread(() -> {
- while (Counter.count == 0) {
-
- }
- System.out.println("t1线程结束");
- });
- Thread t2 = new Thread(() -> {
- System.out.println("输入一个数:");
- Scanner scanner = new Scanner(System.in);
- Counter.count = scanner.nextInt();
-
- });
- t1.start();
- t2.start();
- }
- }
加了关键字之后,输入一个值改变coun值后,循环结束
Java的内存模型JMM(Java Memory Model)
volatile禁止了编译器优化,避免了直接读取CPU寄存器中缓存的数据,而是每次都重新读内存。
用Java的术语来说,应该是:站在JMM的角度看待volatile,正常的程序执行过程中,会把主内存的数据,先加载到工作内存中,再进行计算处理,编译器优化可能会导致不是每次都真的去读取主内存,而直接读取工作内存中的缓存数据(可能导致内存可见性问题),而volatile起到的效果,就是保证每次读取内存都是真的从主存中重新读取
- //不保证原子性
- public class Test2 {
- public static volatile int count;
-
- public static void main(String[] args) {
- Thread t1 = new Thread(() -> {
- while(count<100) {
- System.out.println(count++);
- try {
- Thread.sleep(100);
- } catch (InterruptedException e) {
- throw new RuntimeException(e);
- }
- }
- });
- Thread t2 = new Thread(() -> {
- while(count<100) {
- System.out.println(count++);
- try {
- Thread.sleep(100);
- } catch (InterruptedException e) {
- throw new RuntimeException(e);
- }
- }
- });
- t1.start();
- t2.start();
- }
- }
这里依旧会出现两个线程输出的count的值相同
以单例模式举例:
- public class Singleton {
- private volatile static Singleton instance;
- private Singleton (){}
- public static Singleton getInstance() {
- if (instance == null) {
- synchronized (Singleton.class) {
- if (instance == null) {
- instance = new Singleton();
- }
- }
- }
- return instance;
- }
- }
代码分析:
双锁模式,进行了两次的判断,第一次是判断是否要加锁,第二次是判断是否要创建实例。由于singleton=new Singleton()对象的创建在JVM中可能会进行重排序,例如:new对象时,大致可以分为三个步骤:
此时可能会出现编译器自动优化,将步骤2和3调换顺序,而刚好在步骤1、3执行完,步骤2未执行时,另一个线程调用getInstance,这时会认为instance非空,直接返回instance,并且在后续可能会针对instance进行解引用操作,而解决这样的问题,办法就是禁止指令重排 ,使用volatile修饰signleton实例变量有效,解决该问题。
下期见!!!
