目录
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);
}
我们再次运行时,编译器又没有对其进行优化了
1、volatile含义
volatile是Java提供的一种轻量级的同步机制。Java 语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量,相比于synchronized(synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。但是volatile 变量的同步性较差(有时它更简单并且开销更低),而且其使用也更容易出错。
volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在JVM底层volatile是采用“内存屏障”来实现的。观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
- 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
- 它会强制将对缓存的修改操作立即写入主存;
- 如果是写操作,它会导致其他CPU中对应的缓存行无效。
2、volatile三个特性:
关于这三个特性是什么意思,这篇博客里面有说明:http://t.csdn.cn/LN8qP
(1、保证可见性
//保证内存可见性
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起到的效果,就是保证每次读取内存都是真的从主存中重新读取
(2、不保证原子性
//不保证原子性
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的值相同
(3、禁止指令重排序
以单例模式举例:
单例模式的双重锁中要加volatile
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对象时,大致可以分为三个步骤:
- 申请内存,得到内存首地址
- 调用构造方法,初始化实例
- 把内存的首地址赋值给instance引用
此时可能会出现编译器自动优化,将步骤2和3调换顺序,而刚好在步骤1、3执行完,步骤2未执行时,另一个线程调用getInstance,这时会认为instance非空,直接返回instance,并且在后续可能会针对instance进行解引用操作,而解决这样的问题,办法就是禁止指令重排 ,使用volatile修饰signleton实例变量有效,解决该问题。
下期见!!!