最早接触到 volatile 的关键字的时候, 是用在多线程控制地方,一个主的线程通过 quitFlag 标志来控制子线程的启停,子线程通过循环来判断标记是否为 true,为 true 则退出,这时候如果不用 volatile 关键字修饰 quitFlag 在主线程更改后, 子线程可能无法立刻看到修改,导致无法及时退出的问题,甚至无法退出的问题。
一 volatile 保障了可见性
上面情况,如果用 volatile 来修饰 quitFlag 关键字,则可以及时退出。
public class TestQuitFlag {
// 这种可能无法即时退出
// private static boolean quitFlag = false;
// 这种情况可以正常退出
private static volatile boolean quitFlag = false;
public static void main(String [] args) throws InterruptedException {
new Thread(){
@Override
public void run() {
while (!quitFlag) {
System.out.println(Thread.currentThread()+" is running");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Child thread is stop");
}
}.start();
Thread.sleep(3000);
quitFlag = true;
System.out.println("Main is exit..");
}
}
原因是 Java 对缓存进行了抽象,java 的 JMM 内存模型,将线程访问的内存分为工作内存和主内存,工作内存只有本线程才可以操作,Java 操作的数据先保存到本地内存中,更改后刷新到主内存中,其他线程读取变量的时候每次都从主内存中同步到它的本地内存中,如下图:
二 volatile 与线程安全
volatile 保障了可见性,不具有原子性,不能保障线程的安全。有些说法可以部分保障线程安全,我认为那种可见性不能算是线程安全。简单的测试下,累加这种典型的场景:
import java.util.ArrayList;
import java.util.List;
public class TestCounter {
private static volatile int count = 0;
public static void main(String [] args) throws InterruptedException {
List<Thread> threads = new ArrayList<>();
for (int i = 0; i< 10 ; i++) {
threads.add(new Thread(){
@Override
public void run() {
for (int j = 0; j < 1000; j++) {
count++;
}
}
});
}
threads.forEach(thread->{thread.start();});
threads.forEach(thread->{
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println("Main is exit..");
System.out.println("result:" + count);
}
}
一共启动 10 个线程,每个线程计数 1000 次,如果是线程安全的结果应该是 10000,打印结果如下:
如果改动下,通过 synchronized 来控制累加,代码如下:
import java.util.ArrayList;
import java.util.List;
public class TestQuitFlag {
private static volatile int count =0 ;
public static void main(String [] args) throws InterruptedException {
List<Thread> threads = new ArrayList<>();
for (int i = 0; i< 10 ; i++) {
threads.add(new Thread(){
@Override
public void run() {
for (int j = 0; j < 1000; j++) {
synchronized (TestQuitFlag.class) {
count++;
}
}
}
});
}
threads.forEach(thread->{thread.start();});
threads.forEach(thread->{
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println("Main is exit..");
System.out.println("result:" + count);
}
}
通过 synchronized 包下代码块,执行的结果就是 10000 了,这里面要注意下所有线程的 synchronized 的传入参数要是同一个对象,如果不是,则达不到锁的目的。比如刚才代码中:
synchronized(TestQuitFlag.class) 改成synchronized(this)是操作不同的对象,则达不到锁的目的。
当然在 java 中有性能更高的累加方法,那就是采用 Atomic*系列类,这些类因为采用 CAS 的方式进行加锁,所以性能更好些,这里就不再举例了。
三 volatile 可以防止指令重排
volatile 可以防止指令重排,JVM 虚拟机在执行 Java 字节码的时候,为了提升性能,在不影响程序语义的情况下,会对指令进行重排。当然除了 JVM,编译器或 cpu 都可能会进行指令重排。
典型的代码场景是双重锁检查单例写法,具体展示如下:
public class Singleton {
private static volatile Singleton singleton;
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null ) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
这里面必须给 singleton 添加 volatile 关键字,为什么要添加关键字,这里和 volatile 的防止指令重排问题有关。这里面主要和singleton = new Singleton();
这句代码相关, 这句代码实际执行的时候分为三步操作:
申请一块内存。
调用 Singleton 的构造函数初始化。
将 singleton 引用指向这块内存空间,这样 singleton 执行就不是 null 了。这三步处于锁控制的范围内,当时如果没有 volatile 情况下,会发生指令重排,而引起错误。来举个例子:1) 线程 1 执行到 synchronized 同步代码块中,判断 singleton 为 null,这时候开始执行
singleton = new Singleton();
代码。2) 由于指令产生了重排,所以执行的代码顺序是 1->3->2 , 执行完 3,之后 singleton 不是 null 了,这时候线程时间片时间到,线程休眠。3) 其他线程再调用getInstance()
判断 singleton 不为 null,直接返回 singleton 使用,当时我们知道,其实这个变量现在是未初始化的。其他线程使用了这个未初始化的变量,从而造成问题。
volatile 关键字给 JVM 指明修饰的字段可能在其他的线程中发生修改,所以
如下图:加上 volatile 关键字后,看 JVM 编译后的代码会多一句:
lock addr $0x0,(%esp)
这个指令相当于一个内存屏障,只有一个 cpu,并不需要;如果有两个或两个以上 cpu 访问访问的时候,会将 cache 本地内存的数据同步到主内存中,通过这个操作让 volatile 变量在其他的内存中立刻可见,也保证了后续的指令不能重排到 lock 指令之前。
顺便说下,DCL 实现的单例模式,还常被问到的点,为什么两次判断 singleton 是否为 null。顺便说下:
第一次判断 singleton 是否为 null,在不为 null 的时候可以不用进入到同步代码块,快速返回,提升了性能。
第二次判断 singleton 是否为 null,一个线程在判断 singleton 为 null,进入到同步代码块之前休眠了,这时候另外一个线程因为判断 singleton 为 null,则先进入了同步代码块,执行完毕后;开始的线程仍然可以进入同步代码块,如果不判断 singleton 是否为 null,则会再次创建个单例对象,违反了我们的单例的初衷。