前言
今天下午BOSS上投了个简历小试了一波水,结果被问到一个知识点volatile关键字的作用,我回答了线程的可见性,另一个死活想不起来是什么,当回到工位上看了眼笔记,才想起来。这种知识点其实平时使用的频率还是挺高的,但是一般很少去探究它的原理,导致今天吃了亏。
Volatile关键字是什么?
Volatile是java提供对内存模型访问的一些特殊的访问规则。
当一个变量被定义为volatile时,该变量将有两成含义
1、线程间的可见性。
当一个线程对一个变量做出改变时,另一个线程可以使用到被改变后的值。但是呢,这里有个问题,先写一段测试代码看下:
public class ThreadTest {
private static int j = 10;
public static void main(String[] args) {
testNoVolatile();
}
private static void testNoVolatile() {
for (int i=0; i<10; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
ThreadUtils.doSleep(10000L);
System.out.println(j--);
}
});
thread.start();
}
}
}
运行结果:
当添加了volatile关键字之后:
public class ThreadTest {
private static volatile int j = 10;
public static void main(String[] args) {
testVolatile();
}
private static void testVolatile() {
for (int i=0; i<10; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
ThreadUtils.doSleep(10000L);
System.out.println(j--);
}
});
thread.start();
}
}
}
发现了什么问题,加了volatile和没加一样,跑出来的都不是我想要的,所以这里一直存在一个误区,包括我在内的好多开发者在刚接触这个关键字的时候都认为如果使用了volatile修饰变量则可以保证线程安全,这样理解其实是忽略了一个重要的问题,那就是java的运算符操作并不是原子的,虽然volatile保证了工作内存和主存的一致性,但是当执行j–时,j–会产生多个指令,这些指令执行时是非原子性的。所以执行上面类似的操作还是要使用synchronized等锁机制来保证原子性。
2、禁止指令重排序
指令重排序顾名思义就是将指令原本的执行顺序打乱。为什么要打乱呢,简单写一段伪代码,比如一段java程序。
private static void testVolatile2() {
int i = 0;
int j = 1;
int ij = i + j; // 指令1
int jxi = i*j; // 指令2
}
比如上述代码中指令1和指令2谁先执行都可以,因为两个指令互不相干。实际上程序在运行时也会做类似的打乱指令去执行。但是如果把指令2修改为int ixj= i*j +ij, 那么很明显指令2要依赖指令1,这个时候如果发生了重排序则会导致程序异常。当然了,真实情况下计算机在重排序时要根据上下文依赖关系去重排序,不会发生以上这种情况。重排序的目的是为了减少cpu和内存的交互,尽可能保证cpu和寄存器或者高速缓存去交互。以上代码只是为了简单说明一下什么是指令重排序。
在《深入理解Java虚拟机:JVM高级特性与最佳实践》这本书中,特别提到一个大家都熟悉的案例,那就是使用双重检查锁来实现的单例模式,这个相信大家在业务代码或者框架中经常用到,如下:
public class Demo2 {
private volatile static Demo2 demo2 = null;
private void Demo2() {
}
public static Demo2 getInstance() {
if (demo2 == null) {
synchronized(Demo2.class) {
if (demo2 == null) {
demo2 = new Demo2();
}
}
}
return demo2;
}
}
如果代码中demo2对象不使用volatile修饰,那么很有可能出现因为指令重排序发生异常,因为demo2 = new Demo2()这句中的new并不是一个原子操作。实际上new Demo2的过程如下(借用网上的图):
如果线程 1 获取到锁进入创建对象实例,这个时候发生了指令重排序。当线程1 执行到 t3 时刻,线程 2 刚好进入,由于此时对象已经不为 Null,所以线程 2 可以自由访问该对象。然后该对象还未初始化,所以线程 2 访问时将会发生异常。
并且双重检测锁的还存在其他问题,该问题jdk1.5才被优化。
jvm内存模型中其他特殊的说明
在阅读《深入理解Java虚拟机:JVM高级特性与最佳实践》这本书时,在第12章12.3.4章节时,特别说明了long和double这两个特殊的变量,模型中特别定义了一条宽松的规定:允许虚拟机将没有 被volatile修饰的64位数据的读写操作划分为两次32位的操作来进行这就导致了如果在多线程下不使用volatile修饰时,可能发生异常,因为可能读取到的“半个数据”。
附上原文:
开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第27天,点击查看活动详情