面试突击51:为什么单例一定要加 volatile?
单例模式的实现方法有很多种,如饿汉模式、懒汉模式、静态内部类和枚举等,当面试官问到“为什么单例模式一定要加 volatile?”时,那么他指的是为什么懒汉模式中的私有变量要加 volatile?
懒汉模式指的是对象的创建是懒加载的方式,并不是在程序启动时就创建对象,而是第一次被真正使用时才创建对象。
要解释为什么要加 volatile?我们先来看懒汉模式的具体实现代码:
public class Singleton {
// 1.防止外部直接 new 对象破坏单例模式
private Singleton() {}
// 2.通过私有变量保存单例对象【添加了 volatile 修饰】
private static volatile Singleton instance = null;
// 3.提供公共获取单例对象的方法
public static Singleton getInstance() {
if (instance == null) { // 第 1 次效验
synchronized (Singleton.class) {
if (instance == null) { // 第 2 次效验
instance = new Singleton();
}
}
}
return instance;
}
}
从上述代码可以看出,为了保证线程安全和高性能,代码中使用了两次 if 和 synchronized 来保证程序的执行。那既然已经有 synchronized 来保证线程安全了,为什么还要给变量加 volatile 呢?
在解释这个问题之前,我们先要搞懂一个前置知识:volatile 有什么用呢?
1.volatile 作用
volatile 有两个主要的作用,第一,解决内存可见性问题,第二,防止指令重排序。
1.1 内存可见性问题
所谓内存可见性问题,指的是多个线程同时操作一个变量,其中某个线程修改了变量的值之后,其他线程感知不到变量的修改,这就是内存可见性问题。
而使用 volatile 就可以解决内存可见性问题,比如以下代码,当没有添加 volatile 时,它的实现如下:
private static boolean flag = false;
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
// 如果 flag 变量为 true 就终止执行
while (!flag) {
}
System.out.println("终止执行");
}
});
t1.start();
// 1s 之后将 flag 变量的值修改为 true
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();
}
以上程序的执行结果如下:
然而,以上程序执行了 N 久之后,依然没有结束执行,这说明线程 2 在修改了 flag 变量之后,线程 1 根本没有感知到变量的修改。
那么接下来,我们尝试给 flag 加上 volatile,实现代码如下:
public class volatileTest {
private static volatile boolean flag = false;
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
// 如果 flag 变量为 true 就终止执行
while (!flag) {
}
System.out.println("终止执行");
}
});
t1.start();
// 1s 之后将 flag 变量的值修改为 true
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();
}
}
以上程序的执行结果如下:
从上述执行结果我们可以看出,使用 volatile 之后就可以解决程序中的内存可见性问题了。
1.2 防止指令重排序
指令重排序是指在程序执行过程中,编译器或 JVM 常常会对指令进行重新排序,已提高程序的执行性能。
指令重排序的设计初衷确实很好,在单线程中也能发挥很棒的作用,然而在多线程中,使用指令重排序就可能会导致线程安全问题了。
所谓线程安全问题是指程序的执行结果,和我们的预期不相符。比如我们预期的正确结果是 0,但程序的执行结果却是 1,那么这就是线程安全问题。
而使用 volatile 可以禁止指令重排序,从而保证程序在多线程运行时能够正确执行。
2.为什么要用 volatile?
回到主题,我们在单例模式中使用 volatile,主要是使用 volatile 可以禁止指令重排序,从而保证程序的正常运行。这里可能会有读者提出疑问,不是已经使用了 synchronized 来保证线程安全吗?那为什么还要再加 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 主要是为了防止第 ② 处执行时,也就是“instance = new Singleton()”执行时的指令重排序的,这行代码看似只是一个创建对象的过程,然而它的实际执行却分为以下 3 步:
- 创建内存空间。
- 在内存空间中初始化对象 Singleton。
- 将内存地址赋值给 instance 对象(执行了此步骤,instance 就不等于 null 了)。
试想一下,如果不加 volatile,那么线程 1 在执行到上述代码的第 ② 处时就可能会执行指令重排序,将原本是 1、2、3 的执行顺序,重排为 1、3、2。但是特殊情况下,线程 1 在执行完第 3 步之后,如果来了线程 2 执行到上述代码的第 ① 处,判断 instance 对象已经不为 null,但此时线程 1 还未将对象实例化完,那么线程 2 将会得到一个被实例化“一半”的对象,从而导致程序执行出错,这就是为什么要给私有变量添加 volatile 的原因了。
总结
使用 volatile 可以解决内存可见性问题和防止指令重排序,我们在单例模式中使用 volatile 主要是使用 volatile 的后一个特性(防止指令重排序),从而避免多线程执行的情况下,因为指令重排序而导致某些线程得到一个未被完全实例化的对象,从而导致程序执行出错的情况。
是非审之于己,毁誉听之于人,得失安之于数。
公众号:Java面试真题解析
面试突击51:为什么单例一定要加 volatile?的更多相关文章
- DCL单例为什么要加Volatile
拿一个对象创建赋值来说 class T{ int elem = 1; } T t = new T(); 上段代码转换成汇编码为: 0 new #2 <T> 3 dup 4 invokesp ...
- 牛客网Java刷题知识点之什么是单例模式?解决了什么问题?饿汉式单例(开发时常用)、懒汉式单例(面试时常用)、单例设计模式的内存图解
不多说,直接上干货! 什么是单例设计模式? 解决的问题:可以保证一个类在内存中的对象唯一性,必须对于多个程序使用同一个配置信息对象时,就需要保证该对象的唯一性. 如何保证? 1.不允许其他程序用new ...
- swift -- 单例+ lazy懒加载 + 第三方库
//工具类单例 static let goods : NHGoods = { let good = NHGoods() return good }() //懒加载 lazy var registerB ...
- 如何写出面试官欣赏的Java单例
单例模式是一种常用的软件设计模式.在它的核心结构中只包含一个被称为单例的特殊类.通过单例模式可以保证系统中一个类只有一个实例. 今天我们不谈单例模式的用途,只说一说如果在面试的时候面试官让你敲一段代码 ...
- Java面试 - 什么是单例设计模式,为什么要使用单例设计模式,如何实现单例设计模式(饿汉式和懒汉式)?
什么是单例设计模式? 单例设计模式就是一种控制实例化对象个数的设计模式. 为什么要使用单例设计模式? 使用单例设计模式可以节省内存空间,提高性能.因为很多情况下,有些类是不需要重复产生对象的. 如果重 ...
- 再看 Java 中的单例
此前面试遇到了单例问题,本以为已经背的滚瓜烂熟,没想到被问单例如何避免被反射和序列化破坏,虽然后来还是等到了通知,但还是复习一下单例的实现方式,并学习防止反射和序列化破坏的手段. 基本实现方式 其他相 ...
- Spring源码解析之八finishBeanFactoryInitialization方法即初始化单例bean
Spring源码解析之八finishBeanFactoryInitialization方法即初始化单例bean 七千字长文深刻解读,Spirng中是如何初始化单例bean的,和面试中最常问的Sprin ...
- Javascript 设计模式 单例
转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/30490955 一直很喜欢Js,,,今天写一个Js的单例模式实现以及用法. 1.单 ...
- java双重检测或枚举类实现线程安全单例(懒汉模式)
双重检测实现 /** * 懒汉模式->双重同步锁单例模式 */ public class SingletonExample5 { private SingletonExample5() { } ...
随机推荐
- SVG Sprite 入门(SVG图标解决方案)
关于浏览器图标解决方案,一直就有很多 CSS Sprite,Icon Font,CSS Icon以及SVG.相对而言svg矢量图标以及支持浏览器自身解析的优点,很多团队都已经在使用了.这篇文章主要说明 ...
- JavaScript作用域链与闭包的理解
作用域是JavaScript最重要的概念之一,想要学好JavaScript就需要理解JavaScript作用域和作用域 链的工作原理. 1. 全局作用域(Global Scope) (1)最外层函数和 ...
- [护网杯 2018]easy_tornado 1
复现一道关于tornado的题目 首先可以得知此题用的是tornado,基于python的后端框架,多半是ssti注入 有三个文件,首先可得知flag在何处 然后观察hint和url就知道要根据coo ...
- CCF201403-2窗口
问题描述 在某图形操作系统中,有 N 个窗口,每个窗口都是一个两边与坐标轴分别平行的矩形区域.窗口的边界上的点也属于该窗口.窗口之间有层次的区别,在多于一个窗口重叠的区域里,只会显示位于顶层的窗口里的 ...
- 时序数据库influxDB介绍
https://www.jianshu.com/p/68c471bf5533 https://www.cnblogs.com/wzbk/p/10569683.html
- 小天才XTC Z1S开启ADB
起因 最近入手了Apple Watch,但因系统闭源和国区App Store第三方应用实在是少,所以就开始折腾起安卓表来了.正好家里有块给小孩子用的小天才手表,所以就想到了通过ADB调试安装一些这块表 ...
- JVM虚拟机类加载机制(一)
类从被加载到虚拟机内存中开始,到卸载出内存截止,整个生命周期包括:加载.验证.准备.解析,初始化.使用.卸载七个阶段.其中验证.准备.解析三个部分统称为连接. 类初始化情况: 遇到new.getsta ...
- 小程序监听-data 或者 子组件properties 数据
observers: { 'plateInfo': (obj) => { console.log('监听', obj) if(Object.keys(obj) ...
- 介绍关于MSSQL当前行中获取到上一行某列值的函数 Coalesce
记录一个小知识点,在SQLGrid中,在当前行显示上一行某列值的函数** Coalesce **的使用. 显示上一行是有啥子用? 经常有人百度SQL上一行减下一行的写法,但是没几个文章是用最简单直接的 ...
- 基于深度学习的车辆检测系统(MATLAB代码,含GUI界面)
摘要:当前深度学习在目标检测领域的影响日益显著,本文主要基于深度学习的目标检测算法实现车辆检测,为大家介绍如何利用\(\color{#4285f4}{M}\color{#ea4335}{A}\colo ...