关于volatile
volatile是Java提供的一种轻量级的同步机制,它主要有两个特性:保证变量的可见性和禁止指令重排序。
1. 保证变量的可见性:在Java中,为了提高效率,每个线程都会有自己的工作内存(可以理解为CPU的高速缓存),线程对变量的所有操作都会在工作内存中进行,然后再同步回主内存。如果一个变量被volatile修饰,那么当一个线程修改了这个变量后,新的值会立即同步回主内存,当其他线程需要读取这个变量时,会直接从主内存中读取,而不是从工作内存,这样就保证了变量的可见性。
2. 禁止指令重排序:在执行程序时,为了提高性能,编译器和处理器可能会对指令做重排序。但是,如果对volatile变量的读写,Java内存模型会禁止指令重排序。具体来说,写入volatile变量的操作会在读写操作之前执行,读取volatile变量的操作会在读写操作之后执行。
关于指令重排
在Java中,为了提高程序运行效率,编译器和处理器可能会对指令进行重排序。这种重排序在单线程环境下是没有问题的,因为它不会改变程序的执行结果。但在多线程环境下,指令重排序可能会导致严重的问题。
指令重排主要包括以下三种类型:
1. 编译器优化的重排:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。 2. 指令级并行的重排:现代处理器采用了指令级并行技术(Instruction-Level Parallelism,简称ILP)来提升性能。ILP会在CPU内部对指令进行动态重排。
3. 内存系统的重排:由于处理器使用缓存和读/写缓冲区,这也可能导致处理器看到的内存顺序与实际的顺序不同。
volatile原理
volatile底层原理就是利用内存屏障:
在写volatile的时候,在写后,会进行写屏障。这样一来,在对volatile变量赋值之后,就会执行写屏障,将新的数据刷到主存上。
在读volatile的时候,在读前,就进行读屏障。这样一来,在对volatile变量进行读取的时候,就会执行读屏障,将主存上的数据读取过来。
同时,在写屏障执行之前,jvm确保之前的代码不会出现在写屏障之后。
volatile修饰的变量只能保证在当前线程内,变量的数据是主存上最新的。但是不能保证线程之间的互斥性。当一个线程在读取或者写入volatile变量的时候,通过读写屏障确保数据最新,但是当前线程不能控制另一个线程修改volatile。
DCL
常见的饱汉式单例模式我们在业务中经常会用,一个完美的代码如下:
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
这里面可以发现instance变量用volatile修饰了。那么为什么要用valatile修饰呢?
我们可以看一下没有volatile变量修饰的字节码文件是什么样的
0 getstatic #3 <com/VolatileTest.instance : Lcom/VolatileTest;>
3 ifnonnull 37 (+34)
6 ldc #4 <com/VolatileTest>
8 dup
9 astore_0
10 monitorenter
11 getstatic #3 <com//VolatileTest.instance : Lcom/VolatileTest;>
14 ifnonnull 27 (+13)
17 new #4 <com/VolatileTest>
20 dup
21 invokespecial #5 <com/VolatileTest.<init> : ()V>
24 putstatic #3 <com/VolatileTest.instance : Lcom/VolatileTest;>
27 aload_0
28 monitorexit
29 goto 37 (+8)
32 astore_1
33 aload_0
34 monitorexit
35 aload_1
36 athrow
37 getstatic #3 <com/VolatileTest.instance : Lcom/VolatileTest;>
40 areturn
可以看到 instant = new VolatileTest()这段java代码,在字节码文件中其实是对应多行指令:
1、new 命令。创建对象的实例,但是此时只是为对象分配了空间
2、invokespecial命令。真正执行构造方法
3、putstatic命令。将实例对象赋值给静态变量。
在极端情况下,putstatic和invokespecial可能发生指令重排。如果发生重排,就导致当前instance对象还没有执行构造方法,就将一个空的盒子返回了。
因此,如果其他线程来判断 instance==null 的时候就会返回false。从而导致其他线程拿到了一个空盒的对象,如果利用这个对象去进行操作,那么就会有问题。
而如果加上了volatile修饰,因为有内存屏障,上述指令就不会重排,也就不会出现问题