首先,我们使用 volatile 修饰一个整数变量,再启动两个线程分别执行同样次数的 ++ 和 -- 操作,最后发现执行的结果竟然不是 0,代码如下:
public class VolatileExample { public static volatile int count = 0; // 计数器 public static final int size = 100000; // 循环测试次数 public static void main(String[] args) { // ++ 方式 Thread thread = new Thread(() -> { for (int i = 1; i <= size; i++) { count++; } }); thread.start(); // -- 方式 for (int i = 1; i <= size; i++) { count--; } // 等所有线程执行完成 while (thread.isAlive()) {} System.out.println(count); // 打印结果 } }以上程序执行结果如下:
1065
可以看出,执行结果并不是我们期望的结果 0,我们把以上代码使用 synchronized 改造一下:
public class VolatileExample { public static int count = 0; // 计数器 public static final int size = 100000; // 循环测试次数 public static void main(String[] args) { // ++ 方式 Thread thread = new Thread(() -> { for (int i = 1; i <= size; i++) { synchronized (VolatileExample.class) { count++; } } }); thread.start(); // -- 方式 for (int i = 1; i <= size; i++) { synchronized (VolatileExample.class) { count--; } } // 等所有线程执行完成 while (thread.isAlive()) {} System.out.println(count); // 打印结果 } }这次执行的结果变成了我们期望的值 0。
这说明 volatile 只是轻量级的线程可见方式,并不是轻量级的同步方式,所以并不能说 volatile 是轻量级的 synchronized,终于知道为什么面试官让我回去等通知了。
volatile 使用场景既然 volatile 只能保证线程操作的可见方式,那它有什么用呢?
volatile 在多读多写的情况下虽然一定会有问题,但如果是一写多读的话使用 volatile 就不会有任何问题。volatile 一写多读的经典使用示例就是 CopyOnWriteArrayList,CopyOnWriteArrayList 在操作的时候会把全部数据复制出来对写操作加锁,修改完之后再使用 setArray 方法把此数组赋值为更新后的值,使用 volatile 可以使读线程很快的告知到数组被修改,不会进行指令重排,操作完成后就可以对其他线程可见了,核心源码如下:
本文我们通过代码的方式演示了 volatile 的两大特性,内存可见性和禁止指令重排,使用 ++ 和 -- 的方式演示了 volatile 并非轻量级的同步方式,以及 volatile 一写多读的经典使用案例 CopyOnWriteArrayList。