volatile 关键字,用来解决可见性、有序性问题。被 volatile 关键字修饰的变量,会确保值的变化被其它线程所感知,从而从主存中取得该变量最新的值。在 happans-before 原则中有一条 volatile 变量原则,阐述了 vlatile 如何确保有序性。
volatile 效果
一个例子:主线程试图通过修改 flag 的值,来触发 visableThread 线程打印自己线程 name。代码如下:
public class VolatileTest {
private static class ShowVisibility implements Runnable {
public static Object o = new Object();
private volatile Boolean flag = false;
// private Boolean flag = false;
@Override
public void run() {
while (true) {
if (flag) {
System.out.println(Thread.currentThread().getName() + ":" + flag);
}
}
}
}
public static void main(String[] args) throws InterruptedException {
ShowVisibility showVisibility = new ShowVisibility();
Thread visableThread = new Thread(showVisibility);
visableThread.start();
//给线程启动的时间
Thread.sleep(500);
//更新flay
showVisibility.flag = true;
System.out.println("flag is true, thread should print");
Thread.sleep(1000);
System.out.println("I have slept 1 seconds. Is there anything printed ?");
}
}
使用volatile 修饰 flag 变量和不使用volatile 修饰 flag 变量,有不同的输出结果,如下:
使用volatile 修饰 flag 变量,输出如下:
Thread-0:true
Thread-0:true
Thread-0:true
Thread-0:true
…
不使用volatile 修饰 flag 变量,输出如下:
flag is true, thread should print
I have slept 1 seconds. Is there anything printed ?
代码中使用 volatile 修饰 flag 变量。这确保在多个线程并发时,任何一个线程改变了 flag 的值都会立即被其它线程所看到。以上程序 main 线程修改了 flag 值后,visableThread 能够立即打印出自己的线程 name。但如果我们把 flag 前的 volatile 去掉,可以看到 main 线程修改了 flag 值后,visableThread 也不会有任何输出。也就是说 visableThread 并不知道 flag 值已经被修改。
原因在于,为了提高计算效率,CPU 会从缓存中取得 flag 值。但是主存中 flag 值的变化,visableThread 线程并不知道,导致其缓存和主存不一致,获取到的是失效的 flag 值。
理解volatile
volatile 关键字可以用来修饰实例变量和类变量。被 volatile 修饰后,该变量或获得以下特性:
- 可见性。任何线程对其修改,其它线程马上就能读到最新值;
- 有序性。禁止指令重排序。
CPU 为了提升速度,采用了缓存,因此造成了多个线程缓存不一致的问题,这也是可见性的根源。为了解决缓存一致性,我们需要了解缓存一致性协议。MESI 协议是目前主流的缓存一致性协议。此协议会保证,写操作发生时,线程独占该变量的缓存,CPU 并且会通知其它线程对于该变量所在的缓存段失效。只有在独占操纵完成之后,该线程才能修改此变量。而此时由于其它缓存全部失效,所以就不存在缓存一致性问题。而其它线程的读取操作,需要等写入操作完成,恢复到共享状态。
volatile 能够保证变量的可见性和有序性,但是并不能保证原子性。比如我们用 volatile 修饰了变量 i,多线程并发执行 i++。假如有 10 个线程,每个线程执行 1 万次 i++,那么最后 i 的结果肯定不是 10 万。因为 i++ 实际为三步操作:
- 从主存取得 i 的值,存入缓存;
- 为 i 加 1;
- 赋给 i,写入主存。
这三步在没有原子性保证时多线程并发,就会导致不同线程同时执行了步骤 1,读取到了一样的 n 值,从而造成了重复的 +1 操作。多次 i++ 操作但只为 i 增加了 1。
volatile 的使用场景
volatile 能为我们提供如下特性:
- 确保实例变量和类变量的可见性;
- 确保 volatile 变量前后代码的重排序以 volatile 变量为界限。
volatile 的局限性:
- volatile 的可见性和有序性只能作用于单一变量;
- volatile 不能确保原子性;
- volatile 不能作用于方法,只能修饰实例或者类变量。
volatile 的以上特点,决定了它的使用场景是有限的,并不能完全取代 synchronized 同步方式。一般使用 volatile 的场景是代码中通过某个状态值 flag 做判断,flag 可能被多个线程修改。如果不使用 volatile 修饰,那么 flag 不能保证最新的值被每个线程读取到。而在使用 volatile 修饰后,任何线程对 flag 的修改,都立刻对其它线程可见。此外其它线程看到 flag 变化时,所有对 flag 操作前的代码都已生效,这是 volatile 的有序性确保的。
正是由于 volatile 有如上局限性,所以我们只能在上述场景或者其它适合的场景使用 volatile。反推 volatile 不适用的场景如下:
- 一个变量或者多个变量的原子性操作;
- 不以 volatile 变量操作作为分界线的有序性保证。
volatile 无法解决的问题最终还得通过 sychronized 或者其它加锁方式来确保同步。