博客
关于我
因为我说:volatile 是轻量级的 synchronized,面试官让我回去等通知!
阅读量:468 次
发布时间:2019-03-06

本文共 5336 字,大约阅读时间需要 17 分钟。

Java volatile内核深入理解

在Java的多线程编程中,volatile是一个非常重要的关键字,用于解决并发编程中的内存可见性问题。它通常被用来修饰那些在多个线程之间需要共享的变量,以确保这些变量的可见性和一致性。然而,volatile并不是一个全能的同步机制,它只能提供一种轻量级的可见性保证,不能完全替代传统的同步机制。因此,在面试中,当面试官问及volatile的特性时,仅仅说“volatile是轻量级的synchronized”可能会让人显得不够深入。为了更好地理解和掌握volatile的内核细节,我们将从内存可见性和指令重排两个方面展开讨论,并结合实际案例来分析其使用场景。

内存可见性

在Java的内存模型中,所有共享变量都存储在主内存中,每个线程都有自己的工作内存。为了提高线程运行效率,主内存中的共享变量会被每个线程缓存到其工作内存中。这种做法虽然提高了性能,但也带来了一个潜在的问题:当一个线程修改了主内存中的共享变量后,其他线程可能并不立即意识到这个变量已经被修改。

我们可以通过一个简单的例子来理解这一问题:

public class VolatileExample {    private static boolean flag = false;    public static void main(String[] args) {        Thread t1 = new Thread(() -> {            try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); }            flag = true;            System.out.println("flag 被修改成 true");        });        t1.start();        while (true) {            if (flag) {                System.out.println("检测到 flag 变为 true");                break;            }        }    }}

在这个程序中,主线程一直等待检测到flag变为true,但是由于非主线程修改了flag的值,主线程却无法立即感知到这一变化。这样的现象正是内存不可见性的体现。

使用volatile修饰flag后,情况就发生了变化:

public class VolatileExample {    private static volatile boolean flag = false;    public static void main(String[] args) {        Thread t1 = new Thread(() -> {            try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); }            flag = true;            System.out.println("flag 被修改成 true");        });        t1.start();        while (true) {            if (flag) {                System.out.println("检测到 flag 变为 true");                break;            }        }    }}

这次,主线程能够立即检测到flag的变化,并输出相应的日志。这证明volatile确实能够保证内存的可见性,使得其他线程能够立即知道共享变量的值已经发生了变化。

指令重排

在计算机体系结构中,指令重排是指处理器为了提高性能而对程序指令进行重新排序的现象。虽然指令重排能够提升程序的执行效率,但它也可能导致指令的执行顺序发生变化,从而引发潜在的竞态条件。

我们可以通过以下代码来理解指令重排带来的问题:

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(() -> {                a = 1;                x = b;            });            Thread t2 = new Thread(() -> {                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;        }    }}

在这个程序中,线程t1和t2分别执行以下操作:

  • t1:将a设置为1,然后将x设置为b的值。
  • t2:将b设置为1,然后将y设置为a的值。

观察执行结果,我们可以发现当执行到一定次数(如48526次)时,y变成了一个错误的值0。这表明指令重排导致了数据的不一致性。

通过在ab上使用volatile修饰:

public class VolatileExample {    private static volatile 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(() -> {                a = 1;                x = b;            });            Thread t2 = new Thread(() -> {                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;        }    }}

指令重排的问题得到了有效的解决,y的值将正确地反映ab的状态。

volatile的非同步方式

虽然volatile能够保证内存的可见性,但它并不能提供同步机制。下面我们可以通过一个实际案例来理解这一点:

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

在这个程序中,一个线程(thread)不断对count进行++操作,而另一个线程则进行--操作。由于volatile没有提供同步机制,两个线程可能会交错执行导致count的值无法正确维持在0。

通过使用synchronized修饰count

public class VolatileExample {    public static int count = 0;    public static final int size = 100000;    public static void main(String[] args) {        Thread thread = new Thread(() -> {            synchronized (VolatileExample.class) {                for (int i = 1; i <= size; i++) {                    count++;                }            }        });        thread.start();        for (int i = 1; i <= size; i++) {            synchronized (VolatileExample.class) {                count--;            }        }        while (thread.isAlive()) {}        System.out.println(count);    }}

现在,count的值可以正确维持在0。这表明volatile不是一个同步机制,只能提供可见性保证。

volatile的使用场景

volatile的主要使用场景是在多读多写的情况下,确保共享变量的可见性。然而,如果变量的修改频繁且读线程相对独立,volatile可以有效地避免指令重排带来的问题。在实际应用中,volatile经常用于实现如CopyOnWriteArrayList这样的并发容器。这种容器在每次写操作时会复制整个数组,并在操作完成后通过setArray方法将新数组替换原数组。volatile确保了读线程能够立即感知到数组的更新,从而避免了指令重排的问题。

总结

通过对volatile内存可见性和指令重排的深入分析,以及对其非同步特性的理解,我们可以得出以下结论:

  • 内存可见性volatile确保了线程之间的共享变量修改能够被其他线程立即感知。
  • 指令重排volatile能够禁止处理器对指令进行重排序,避免指令执行顺序的变化带来的数据不一致性。
  • 非同步特性volatile不能替代传统的同步机制,不能保证线程的互斥访问。
  • 使用场景volatile在多读多写场景下提供了一种轻量级的可见性保障,非常适用于诸如CopyOnWriteArrayList这样的并发容器。
  • 通过这些理解,我们可以更好地在实际项目中灵活运用volatile,在需要的时候利用其优点,同时避免其不足之处,从而实现高效且安全的并发编程。

    转载地址:http://qrgfz.baihongyu.com/

    你可能感兴趣的文章
    nodejs封装http请求
    查看>>
    nodejs开发公众号报错 40164,白名单配置找不到,竟然是这个原因
    查看>>
    Nodejs异步回调的处理方法总结
    查看>>
    NodeJS报错 Fatal error: ENOSPC: System limit for number of file watchers reached, watch ‘...path...‘
    查看>>
    Nodejs教程09:实现一个带接口请求的简单服务器
    查看>>
    nodejs服务端实现post请求
    查看>>
    nodejs框架,原理,组件,核心,跟npm和vue的关系
    查看>>
    Nodejs模块、自定义模块、CommonJs的概念和使用
    查看>>
    nodejs端口被占用原因及解决方案
    查看>>
    Nodejs简介以及Windows上安装Nodejs
    查看>>
    nodejs系列之express
    查看>>
    nodejs系列之Koa2
    查看>>
    Nodejs连接mysql
    查看>>
    nodejs连接mysql
    查看>>
    NodeJs连接Oracle数据库
    查看>>
    nodejs配置express服务器,运行自动打开浏览器
    查看>>
    Nodemon 深入解析与使用
    查看>>
    node不是内部命令时配置node环境变量
    查看>>
    node中fs模块之文件操作
    查看>>
    Node中同步与异步的方式读取文件
    查看>>