volatile 是 Java 并发编程的重要组成部分,也是常见的面试题之一,它的主要作用有两个:保证内存的可见性和禁止指令重排序。下面我们具体来看这两个功能。

内存可见性

说到内存可见性问题就不得不提 Java 内存模型,Java 内存模型(Java Memory Model)简称为 JMM,主要是用来屏蔽不同硬件和操作系统的内存访问差异的,因为在不同的硬件和不同的操作系统下,内存的访问是有一定的差异得,这种差异会导致相同的代码在不同的硬件和不同的操作系统下有着不一样的行为,而 Java 内存模型就是解决这个差异,统一相同代码在不同硬件和不同操作系统下的差异的。

Java 内存模型规定:所有的变量(实例变量和静态变量)都必须存储在主内存中,每个线程也会有自己的工作内存,线程的工作内存保存了该线程用到的变量和主内存的副本拷贝,线程对变量的操作都在工作内存中进行。线程不能直接读写主内存中的变量,如下图所示:



然而,Java 内存模型会带来一个新的问题,那就是内存可见性问题,也就是当某个线程修改了主内存中共享变量的值之后,其他线程不能感知到此值被修改了,它会一直使用自己工作内存中的“旧值”,这样程序的执行结果就不符合我们的预期了,这就是内存可见性问题,我们用以下代码来演示一下这个问题:

private static boolean flag = false;
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (!flag) { }
System.out.println("终止执行");
}
});
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("设置 flag=true");
flag = true;
}
});
t2.start();
}

以上代码我们预期的结果是,在线程 1 执行了 1s 之后,线程 2 将 flag 变量修改为 true,之后线程 1 终止执行,然而,因为线程 1 感知不到 flag 变量发生了修改,也就是内存可见性问题,所以会导致线程 1 会永远的执行下去,最终我们看到的结果是这样的:



如何解决以上问题呢?只需要给变量 flag 加上 volatile 修饰即可,具体的实现代码如下:

private volatile static boolean flag = false;
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (!flag) { }
System.out.println("终止执行");
}
});
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("设置 flag=true");
flag = true;
}
});
t2.start();
}

以上程序的执行结果如下图所示:

禁止指令重排序

指令重排序是指编译器或 CPU 为了优化程序的执行性能,而对指令进行重新排序的一种手段。

指令重排序的实现初衷是好的,但是在多线程执行中,如果执行了指令重排序可能会导致程序执行出错。指令重排序最典型的一个问题就发生在单例模式中,比如以下问题代码:

public class Singleton {
private Singleton() {}
private static Singleton instance = null;
public static Singleton getInstance() {
if (instance == null) { // ①
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); // ②
}
}
}
return instance;
}
}

以上问题发生在代码 ② 这一行“instance = new Singleton();”,这行代码看似只是一个创建对象的过程,然而它的实际执行却分为以下 3 步:

  1. 创建内存空间。
  2. 在内存空间中初始化对象 Singleton。
  3. 将内存地址赋值给 instance 对象(执行了此步骤,instance 就不等于 null 了)。

如果此变量不加 volatile,那么线程 1 在执行到上述代码的第 ② 处时就可能会执行指令重排序,将原本是 1、2、3 的执行顺序,重排为 1、3、2。但是特殊情况下,线程 1 在执行完第 3 步之后,如果来了线程 2 执行到上述代码的第 ① 处,判断 instance 对象已经不为 null,但此时线程 1 还未将对象实例化完,那么线程 2 将会得到一个被实例化“一半”的对象,从而导致程序执行出错,这就是为什么要给私有变量添加 volatile 的原因了。

要使以上单例模式变为线程安全的程序,需要给 instance 变量添加 volatile 修饰,它的最终实现代码如下:

public class Singleton {
private Singleton() {}
// 使用 volatile 禁止指令重排序
private static volatile Singleton instance = null; // 【主要是此行代码发生了变化】
public static Singleton getInstance() {
if (instance == null) { // ①
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); // ②
}
}
}
return instance;
}
}

总结

volatile 是 Java 并发编程的重要组成部分,它的主要作用有两个:保证内存的可见性和禁止指令重排序。volatile 常使用在一写多读的场景中,比如 CopyOnWriteArrayList 集合,它在操作的时候会把全部数据复制出来对写操作加锁,修改完之后再使用 setArray 方法把此数组赋值为更新后的值,使用 volatile 可以使读线程很快的告知到数组被修改,不会进行指令重排,操作完成后就可以对其他线程可见了。

是非审之于己,毁誉听之于人,得失安之于数。

公众号:Java面试真题解析

面试合集:https://gitee.com/mydb/interview

面试突击44:volatile 有什么用?的更多相关文章

  1. 《【面试突击】— Redis篇》--Redis都有哪些数据类型?分别在哪些场景下使用比较合适?

    能坚持别人不能坚持的,才能拥有别人不能拥有的.关注编程大道公众号,让我们一同坚持心中所想,一起成长!! <[面试突击]— Redis篇>--Redis都有哪些数据类型?分别在哪些场景下使用 ...

  2. 《【面试突击】— Redis篇》-- Redis的线程模型了解吗?为啥单线程效率还这么高?

    能坚持别人不能坚持的,才能拥有别人未曾拥有的.关注编程大道公众号,让我们一同坚持心中所想,一起成长!! <[面试突击]— Redis篇>-- Redis的线程模型了解吗?为啥单线程效率还这 ...

  3. 《【面试突击】— Redis篇》-- Redis的主从复制?哨兵机制?

    能坚持别人不能坚持的,才能拥有别人未曾拥有的.关注左上角编程大道公众号,让我们一同坚持心中所想,一起成长!! <[面试突击]— Redis篇>-- Redis的主从复制?哨兵机制? 在这个 ...

  4. 《【面试突击】— Redis篇》-- Redis哨兵原理及持久化机制

    能坚持别人不能坚持的,才能拥有别人未曾拥有的.关注编程大道公众号,让我们一同坚持心中所想,一起成长!! <[面试突击]— Redis篇>-- Redis哨兵原理及持久化机制 在这个系列里, ...

  5. 《【面试突击】— Redis篇》--Redis Cluster及缓存使用和架构设计的常见问题

    能坚持别人不能坚持的,才能拥有别人未曾拥有的.关注编程大道公众号,让我们一同坚持心中所想,一起成长!! <[面试突击]— Redis篇>--Redis Cluster及缓存使用和架构设计的 ...

  6. 面试中的volatile关键字

    在Java的面试当中,面试官最爱问的就是volatile关键字相关的内容.经过多次面试之后,你是否思考过,为什么他们那么爱问volatile关键字相关的问题?而对于你,如果作为面试官,是否也会考虑采用 ...

  7. 面试官:volatile关键字用过吧?说一下作用和实现吧

    volatile    可见性的本质类似于CPU的缓存一致性问题,线程内部的副本类似于告诉缓存区 面试官:volatile关键字用过吧?说一下作用和实现吧 https://blog.csdn.net/ ...

  8. 面试突击51:为什么单例一定要加 volatile?

    单例模式的实现方法有很多种,如饿汉模式.懒汉模式.静态内部类和枚举等,当面试官问到"为什么单例模式一定要加 volatile?"时,那么他指的是为什么懒汉模式中的私有变量要加 vo ...

  9. Oracle DBA面试突击题

    一份ORACLE DBA面试题 一:SQL tuning 类 1:列举几种表连接方式 答: Oracle的多表连接算法有Nest Loop.Sort Merge和Hash Join三大类,每一类又可以 ...

随机推荐

  1. Django获取请求的IP地址

    if request.META.get('HTTP_X_FORWARDED_FOR'): ip = request.META.get("HTTP_X_FORWARDED_FOR") ...

  2. redis持久存储RDB和AOF的区别及优缺点

    1.前言 最近在项目中使用到Redis做缓存,方便多个业务进程之间共享数据.由于Redis的数据都存放在内存中,如果没有配置持久化,redis重启后数据就全丢失了,于是需要开启redis的持久化功能, ...

  3. VMware ESXi安装NVIDIA GPU显卡硬件驱动和配置vGPU

    一.驱动软件准备:从nvidia网站下载驱动,注意,和普通显卡下载驱动地址不同. 按照ESXi对应版本不同下载不同的安装包.安装包内含ESXi主机驱动和虚拟机驱动. GPU显卡和物理服务器兼容查询:( ...

  4. 插值方法 - Lagrange插值多项式

    Lagrange插值多项式代码: 1 # -*- coding: utf-8 -*- 2 """ 3 Created on Wed Mar 25 15:43:42 202 ...

  5. canvas绘图API详解

    canvas绘图API详解 1.context的状态 矩阵变换属性 当前剪辑区域 context的其他状态属性: strokeStyle, fillStyle, globalAlpha, lineWi ...

  6. 实现拖拽复制和可排序的react.js组件

    在实现复制前,对之前的拖拽排序组件属性进行了修改. 摒弃了value中的content属性,拖拽组件暴露的render函数,利用这个属性进行组件内部子组件的渲染,这点主要是参考了蚂蚁金服的Ant de ...

  7. Spark入门之idea编写Scala脚本

    一.安装Scala插件 1.File->Settings 2.Plugins->Msrketplace->搜索Scala并安装 (或者自己下载合适的scala版本,教程:自己给ide ...

  8. MapReduce在集群执行任务时报错:Initialization of all the collectors failed. Error in last collector was:java.lang.ClassCastException

    报错信息详细: Error: java.io.IOException: Initialization of all the collectors failed. Error in last colle ...

  9. Android项目如何打包成安装包.apk

    1.Build -> Generate Signed APK 2.APK -> Next 3.输入Key store path以及密码 ->next(如果是第一次则需要新建,新建教程 ...

  10. EFCore 6.0入门看这篇就够了

    前言 作为一直在dotNet行业耕耘的码农,这几年在大大小小项目中也涉及到了许多ORM框架,比如:EFCore,Dapper,NHibernate,SqlSugar等等,这些ORM都有各自的优缺点,大 ...