volatile 是 Java 并发编程的重要组成部分,也是常见的面试题之一,它的次要作用有两个:保障内存的可见性和禁止指令重排序。上面咱们具体来看这两个性能。
内存可见性
说到内存可见性问题就不得不提 Java 内存模型,Java 内存模型(Java Memory Model)简称为 JMM,次要是用来屏蔽不同硬件和操作系统的内存拜访差别的,因为在不同的硬件和不同的操作系统下,内存的拜访是有肯定的差别得,这种差别会导致雷同的代码在不同的硬件和不同的操作系统下有着不一样的行为,而 Java 内存模型就是解决这个差别,对立雷同代码在不同硬件和不同操作系统下的差别的。
Java 内存模型规定:所有的变量(实例变量和动态变量)都必须存储在主内存中,每个线程也会有本人的工作内存,线程的工作内存保留了该线程用到的变量和主内存的正本拷贝,线程对变量的操作都在工作内存中进行。线程不能间接读写主内存中的变量,如下图所示:
然而,Java 内存模型会带来一个新的问题,那就是内存可见性问题,也就是当某个线程批改了主内存中共享变量的值之后,其余线程不能感知到此值被批改了,它会始终应用本人工作内存中的“旧值”,这样程序的执行后果就不合乎咱们的预期了,这就是内存可见性问题,咱们用以下代码来演示一下这个问题:
private static boolean flag = false;
public static void main(String[] args) {Thread t1 = new Thread(new Runnable() {
@Override
public void run() {while (!flag) { }
System.out.println("终止执行");
}
});
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {Thread.sleep(1000);
} catch (InterruptedException e) {e.printStackTrace();
}
System.out.println("设置 flag=true");
flag = true;
}
});
t2.start();}
以上代码咱们预期的后果是,在线程 1 执行了 1s 之后,线程 2 将 flag 变量批改为 true,之后线程 1 终止执行,然而,因为线程 1 感知不到 flag 变量产生了批改,也就是内存可见性问题,所以会导致线程 1 会永远的执行上来,最终咱们看到的后果是这样的:
如何解决以上问题呢?只须要给变量 flag 加上 volatile 润饰即可,具体的实现代码如下:
private volatile static boolean flag = false;
public static void main(String[] args) {Thread t1 = new Thread(new Runnable() {
@Override
public void run() {while (!flag) { }
System.out.println("终止执行");
}
});
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {Thread.sleep(1000);
} catch (InterruptedException e) {e.printStackTrace();
}
System.out.println("设置 flag=true");
flag = true;
}
});
t2.start();}
以上程序的执行后果如下图所示:
禁止指令重排序
指令重排序是指编译器或 CPU 为了优化程序的执行性能,而对指令进行从新排序的一种伎俩。
指令重排序的实现初衷是好的,然而在多线程执行中,如果执行了指令重排序可能会导致程序执行出错。指令重排序最典型的一个问题就产生在单例模式中,比方以下问题代码:
public class Singleton {private Singleton() {}
private static Singleton instance = null;
public static Singleton getInstance() {if (instance == null) { // ①
synchronized (Singleton.class) {if (instance == null) {instance = new Singleton(); // ②
}
}
}
return instance;
}
}
以上问题产生在代码 ② 这一行“instance = new Singleton();”,这行代码 看似只是一个创建对象的过程,然而它的理论执行却分为以下 3 步:
- 创立内存空间。
- 在内存空间中初始化对象 Singleton。
- 将内存地址赋值给 instance 对象(执行了此步骤,instance 就不等于 null 了)。
如果此变量不加 volatile,那么线程 1 在执行到上述代码的第 ② 处时就可能会执行指令重排序,将本来是 1、2、3 的执行程序,重排为 1、3、2。然而非凡状况下,线程 1 在执行完第 3 步之后,如果来了线程 2 执行到上述代码的第 ① 处,判断 instance 对象曾经不为 null,但此时线程 1 还未将对象实例化完,那么线程 2 将会失去一个被实例化“一半”的对象,从而导致程序执行出错,这就是为什么要给公有变量增加 volatile 的起因了。
要使以上单例模式变为线程平安的程序,须要给 instance 变量增加 volatile 润饰,它的最终实现代码如下:
public class Singleton {private Singleton() {}
// 应用 volatile 禁止指令重排序
private static volatile Singleton instance = null; //【次要是此行代码产生了变动】public static Singleton getInstance() {if (instance == null) { // ①
synchronized (Singleton.class) {if (instance == null) {instance = new Singleton(); // ②
}
}
}
return instance;
}
}
总结
volatile 是 Java 并发编程的重要组成部分,它的次要作用有两个:保障内存的可见性和禁止指令重排序。volatile 常应用在一写多读的场景中,比方 CopyOnWriteArrayList 汇合,它在操作的时候会把全副数据复制进去对写操作加锁,批改完之后再应用 setArray 办法把此数组赋值为更新后的值,应用 volatile 能够使读线程很快的告知到数组被批改,不会进行指令重排,操作实现后就能够对其余线程可见了。
是非审之于己,毁誉听之于人,得失安之于数。
公众号:Java 面试真题解析
面试合集:https://gitee.com/mydb/interview