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

volatile 是并发编程的重要组成部分,也是面试常被问到的问题之一。不要向小强那样,由于一句:volatile 是轻量级的 synchronized,而与指望已久的大厂失之交臂。
volatile 有两大特性:保证内存的可见性和禁止指令重排序。那什么是可见性和指令重排呢?接下来咱们一块儿来看。
内存可见性java

要了解内存可见性先要从 Java 内存模型(JMM)提及,在 Java 中全部的共享变量都在主内存中,每一个线程都有本身的工做内存,为了提升线程的运行速度,每一个线程的工做内存都会把主内存中的共享变量拷贝一份进行缓存,以此来提升运行效率,内存布局以下图所示:面试

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

但这样就会产生一个新的问题,若是某个线程修改了共享变量的值,其余线程不知道此值被修改了,就会发生两个线程值不一致的状况,咱们用代码来演示一下这个问题。编程

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,就能够保证内存的可见性,代码以下:并发

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
指令重排dom

指令重排是指在执行程序时,编译器和处理器经常会对指令进行重排序,已到达提升程序性能的目的。好比小强要去图书馆还上次借的书,随便再借一本新书,而此时室友小王也想让小强帮他还一本书,未发生指令重排的作法是,小强先把本身的事情办完,再去办室友的事,这样显然比较浪费时间,还有一种作法是,他先把本身的书和小王的书一块儿还掉,再给本身借一本新书,这就是指令重排的意义。
但指令重排不能保证指令执行的顺序,这就会形成新的问题,以下代码所示:ide

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;
        }
    }
}

以上程序执行结果以下所示:
由于我说:volatile 是轻量级的 synchronized,面试官让我回去等通知!布局

能够看出执行到 48526 次时发生了指令重排,y 就变成了非正确值 0,显然这不是咱们想要的结果,这个时候就能够使用 volatile 来禁止指令重排。
以上咱们经过代码的方式演示了指令重排和内存可见性的问题,接下来咱们用代码来演示一下 volatile 同步方式的问题。
volatile 非同步方式性能

首先,咱们使用 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 能够使读线程很快的告知到数组被修改,不会进行指令重排,操做完成后就能够对其余线程可见了,核心源码以下:

public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {

    private transient volatile Object[] array;

    final void setArray(Object[] a) {
        array = a;
    }   
    //...... 忽略其余代码
}

总结

本文咱们经过代码的方式演示了 volatile 的两大特性,内存可见性和禁止指令重排,使用 ++ 和 -- 的方式演示了 volatile 并不是轻量级的同步方式,以及 volatile 一写多读的经典使用案例 CopyOnWriteArrayList。
原创不易,期待你的素质三连,ღ( ´・ᴗ・` )比心~
【END】
近期热文

有人说:轻量级锁必定比重量级锁快!我笑了
HashMap 为何会致使 CPU 100%?面试突击 006 期
面试突击 005 | Redis 是如何实现高可用的?它的实现方式有哪些?
关注下方二维码,订阅更多精彩内容

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

相关文章
相关标签/搜索