亲宝软件园·资讯

展开

因为我说:volatile 是轻量级的 synchronized,面试官让我回去等通知!

王磊的博客 人气:4
# 因为我说:volatile 是轻量级的 synchronized,面试官让我回去等通知! > volatile 是并发编程的重要组成部分,也是面试常被问到的问题之一。不要向小强那样,因为一句:volatile 是轻量级的 synchronized,而与期望已久的大厂失之交臂。 volatile 有两大特性:保证内存的可见性和禁止指令重排序。那什么是可见性和指令重排呢?接下来我们一起来看。 ## 内存可见性 要了解内存可见性先要从 Java 内存模型(JMM)说起,在 Java 中所有的共享变量都在主内存中,每个线程都有自己的工作内存,为了提高线程的运行速度,每个线程的工作内存都会把主内存中的共享变量拷贝一份进行缓存,以此来提高运行效率,内存布局如下图所示: ![内存可见性.png](https://cdn.nlark.com/yuque/0/2020/png/92791/1584251030765-4000ef48-1887-4c03-9912-a4e035a1e1bc.png#align=left&display=inline&height=511&name=%E5%86%85%E5%AD%98%E5%8F%AF%E8%A7%81%E6%80%A7.png&originHeight=511&originWidth=777&size=44249&status=done&style=none&width=777) 但这样就会产生一个新的问题,如果某个线程修改了共享变量的值,其他线程不知道此值被修改了,就会发生两个线程值不一致的情况,我们用代码来演示一下这个问题。 ```java public class VolatileExample { // 可见性参数 private static boolean flag = false; public static void main(String[] args) { new Thread(() -> { try { // 暂停 0.5s 执行 Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } flag = true; System.out.println("flag 被修改成 true"); }).start(); // 一直循环检测 flag=true while (true) { if (flag) { System.out.println("检测到 flag 变为 true"); break; } } } } ``` 以上程序的执行结果如下: > flag 被修改成 true 我们会发现永远等不到 `检测到 flag 变为 true` 的结果,这是因为非主线程更改了 flag=true,但主线程一直不知道此值发生了改变,这就是内存不可见的问题。 **内存的可见性**是指线程修改了变量的值之后,其他线程能立即知道此值发生了改变。 我们可以使用 volatile 来修饰 flag,就可以保证内存的可见性,代码如下: ```java public class VolatileExample { // 可见性参数 private static volatile boolean flag = false; public static void main(String[] args) { new Thread(() -> { try { // 暂停 0.5s 执行 Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } flag = true; System.out.println("flag 被修改成 true"); }).start(); // 一直循环检测 flag=true while (true) { if (flag) { System.out.println("检测到 flag 变为 true"); break; } } } } ``` 以上程序的执行结果如下: > 检测到 flag 变为 true > flag 被修改成 true ## 指令重排 指令重排是指在执行程序时,编译器和处理器常常会对指令进行重排序,已到达提高程序性能的目的。 比如小强要去图书馆还上次借的书,随便再借一本新书,而此时室友小王也想让小强帮他还一本书,未发生指令重排的做法是,小强先把自己的事情办完,再去办室友的事,这样显然比较浪费时间,还有一种做法是,他先把自己的书和小王的书一起还掉,再给自己借一本新书,这就是指令重排的意义。 但指令重排不能保证指令执行的顺序,这就会造成新的问题,如下代码所示: ```java public class VolatileExample { // 指令重排参数 private static int a = 0, b = 0; private static int x = 0, y = 0; public static void main(String[] args) throws InterruptedException { for (int i = 0; i < Integer.MAX_VALUE; i++) { Thread t1 = new Thread(() -> { // 有可能发生指令重排,先 x=b 再 a=1 a = 1; x = b; }); Thread t2 = new Thread(() -> { // 有可能发生指令重排,先 y=a 再 b=1 b = 1; y = a; }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("第 " + i + "次,x=" + x + " | y=" + y); if (x == 0 && y == 0) { // 发生了指令重排 break; } // 初始化变量 a = 0; b = 0; x = 0; y = 0; } } } ``` 以上程序执行结果如下所示: ![指令重排.png](https://cdn.nlark.com/yuque/0/2020/png/92791/1584247494600-97b7b4ea-71d0-4991-9fe4-8a5a096a72da.png#align=left&display=inline&height=336&name=%E6%8C%87%E4%BB%A4%E9%87%8D%E6%8E%92.png&originHeight=336&originWidth=372&size=17559&status=done&style=none&width=372) 可以看出执行到 48526 次时发生了指令重排,y 就变成了非正确值 0,显然这不是我们想要的结果,这个时候就可以使用 volatile 来禁止指令重排。 以上我们通过代码的方式演示了指令重排和内存可见性的问题,接下来我们用代码来演示一下 volatile 同步方式的问题。 ## volatile 非同步方式 首先,我们使用 volatile 修饰一个整数变量,再启动两个线程分别执行同样次数的 ++ 和 -- 操作,最后发现执行的结果竟然不是 0,代码如下: ```java 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 改造一下: ```java 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 可以使读线程很快的告知到数组被修改,不会进行指令重排,操作完成后就可以对其他线程可见了,核心源码如下: ```java public class CopyOnWriteArrayList implements List, RandomAccess, Cloneable, java.io.Serializable { private transient volatile Object[] array; final void setArray(Object[] a) { array = a; } //...... 忽略其他代码 } ``` ## 总结 本文我们通过代码的方式演示了 volatile 的两大特性,内存可见性和禁止指令重排,使用 ++ 和 -- 的方式演示了 volatile 并非轻量级的同步方式,以及 volatile 一写多读的经典使用案例 CopyOnWriteArrayList。 >更多 **Java 原创文章**,请关注我微信公众号 **「Java中文社群」**

加载全部内容

相关教程
猜你喜欢
用户评论