Java中有一个比较隐晦的关键字volatile
,关于它我来谈谈自己的理解。
volatile
总体来讲有两个作用:
- 保证所修饰的变量对其它线程是立即可见的
- 禁止指令重排序
什么叫立即可见?我们都知道内存要比硬盘快得多,cpu又比内存快得多,在进行IO操作时这三者效率严重不协调怎么办?所以会先从硬盘加载一部分数据缓存到内存中,而内存又会缓存一部分数据到cpu缓存中,以保证IO的高效。
那么在多线程环境中,每个线程都针对一个变量在cpu中有一份缓存,一个线程对该变量的修改只是在本线程有效的,其它线程读取到的还是原先缓存的值,而volatile
关键字可以避免这个问题。
volatile
会在生成的字节码中加入一条lock
指令,这个指令有两个作用:
- 将当前处理器缓存写回到内存
- 使其它线程的cpu缓存失效
这样其它线程再次读取该变量的时候就会重新去内存中获取,得到最新值。
举个栗子:
public class VolatileDemo {
public volatile boolean run = false;
public static void main(String[] args) {
VolatileDemo v2 = new VolatileDemo();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ":" + (i + 1));
}
v2.run = true;
}).start();
new Thread(() -> {
while (!v2.run)
;
System.out.println(Thread.currentThread().getName() + " is running..");
}).start();
}
}
这段代码中开启了两个线程,第一个线程循环十次后把 volatile 修饰的布尔型变量 run 改为 true,第二个线程在 run 变为 true 之前一直自旋,而后再打印下面一行。输出结果:
Thread-0:1
Thread-0:2
Thread-0:3
Thread-0:4
Thread-0:5
Thread-0:6
Thread-0:7
Thread-0:8
Thread-0:9
Thread-0:10
Thread-1 is running..
volatile
相对于synchronized
可以看成是一种更轻量级的锁,效率更高点。你可能会疑问,既然有了volatile
为什么还要有synchronized
?volatile 有一个问题是不能保证原子性,就像 i++ 这样的操作,volatile是保证不了原子性的。
那么再说说它的第二个作用,禁止指令重排序。
先来引一个老套话题,你们平常怎么写单例模式?饿汉式、懒汉式还是传说中双重校验锁式?
懒汉式线程不安全,但是它可以做到懒加载,节约资源。
饿汉式线程安全,但是不能延迟加载,会消耗相对多的资源。
那么双重校验锁相当于结合了上述两者的优点,懒加载的时候加同步锁,如下:
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
if (uniqueInstance == null) {
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
注意到 uniqueInstance 是加了 volatile 修饰的,在此处是很有必要的,因为 uniqueInstance = new Singleton(); 这个操作其实分三步进行:
- 开辟一片内存
- 初始化对象
- 将引用指向内存地址
但是由于 JVM 具有指令重排的特性,有可能执行顺序变为了 1>3>2,这在单线程情况下自然是没有问题。但如果是多线程就有可能其它线程获得的是一个还没有被初始化的对象以致于程序出错。
所以使用 volatile 修饰的目的是禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。