Volatile 简介
volatile作为java关键字之一,其主要作用为在并发场景下,当某个线程更新了使用volatile修饰的变量后,会立即将修改后的值写入主存,其他线程读取的时候可以保证读到的值是最新的,而不是缓存。非volatile修饰的变量在线程并发的情况下不具备这种特性。
Volatile 特性
1、可见性
这里的可见性是指,当某个线程修改了这个变量的值后,其他线程立即可见。需要注意的一点是,java运算并非原子操作,所以无法保证原子性。
举个经典例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| public class Main {
private static volatile int count = 0;
private static final int times = 10000;
private static void add() { for (int i = 0; i < times; ++i) { count++; } }
public static void main(String[] args) { Thread[] threads = new Thread[10]; for (int i = 0; i < 10; ++i) { threads[i] = new Thread(new Runnable() { @Override public void run() { add(); System.out.println(count); } }); }
for (Thread r : threads) { r.start(); } } }
|
最后输出:
1 2 3 4 5 6 7 8 9 10
| 18522 21471 21644 16865 20807 20421 35868 38790 40240 42493
|
可以发现,最后一个线程执行结束的时候值并非预想的10000。原因是因为 count++
表达式是非原子操作,运行时会做拆解:
- 获取count的值,复制到寄存器
- 把前面获取到的count的值+1然后写到内存中
把上面的例子做个改造,也能印证问题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| public class Main {
private static AtomicInteger count = new AtomicInteger(0);
private static final int times = 10000;
private static void add() { for (int i = 0; i < times; ++i) { count.addAndGet(1); } }
public static void main(String[] args) { Thread[] threads = new Thread[10]; for (int i = 0; i < 10; ++i) { threads[i] = new Thread(new Runnable() { @Override public void run() { add(); System.out.println(count); } }); }
for (Thread r : threads) { r.start(); } } }
|
输出:
1 2 3 4 5 6 7 8 9 10
| 45060 50000 47612 47814 45960 67128 79234 87439 90350 100000
|
无论执行多少次,最后一个线程结束时,count的值总是100000。
2、有序性
有序性是指禁止指令重排优化,即程序执行的顺序按照代码的先后顺序执行。指令重排序是编译器和处理器为了高效对程序进行优化的手段,在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。重排序在多线程环境下出现的概率还是挺高的,在关键字上有volatile和synchronized会禁用重排序。
在JVM层,volatile 是采用“内存屏障”来实现的,那么指令重排序时不能把后面的指令重排序到内存屏障之前的位置,所以在执行到内存屏障这句指令时,在它前面的操作已经全部完成。同时,内存屏障会强制将缓存的修改操作立即写入主存,以确保其他线程立即可见。
Volatile 适用场景
- 适用于对变量的写操作不依赖于当前值,对变量的读取操作不依赖于非volatile变量。
- 适用于读多写少的场景。
应用场景
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| class Singleton{ private volatile static Singleton instance = null; private Singleton() { } public static Singleton getInstance() { if(instance==null) { synchronized (Singleton.class) { if(instance==null) instance = new Singleton(); } } return instance; } }
|
为什么要使用volatile 修饰instance??
主要在于 instance = new Singleton()
这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情:
1.给 instance 分配内存
2.调用 Singleton 的构造函数来初始化成员变量
3.将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)。
但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。