面试突击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() { } ...
随机推荐
- react开发教程(三)组件的构建
什么是组件 组件化就好像我们的电脑装机一样,一个电脑由显示器.主板.内存.显卡.硬盘,键盘,鼠标.... 组件化开发有如下的好处:降低整个系统的耦合度,在保持接口不变的情况下,我们可以替换不同的组件快 ...
- Day10 - JS 实现 Checkbox 中按住 Shift 的多选功能
Day10 - JS 实现 Checkbox 中按住 Shift 的多选功能 作者:liyuechun 简介:JavaScript30 是 Wes Bos 推出的一个 30 天挑战.项目免费提供了 3 ...
- ruby 版本管理RVM (ruby version manager)
macOS. 自带的ruby 版本目录权限比较高, 经常有很多 操作需要权限而不能执行 虽然 macOS 自带了一个 ruby 环境,但是是系统自己使用的,所以权限很小,只有 system. 而/Li ...
- 从数据库中获取图片编号,然后通过request获取图片下载
import pandas as pd from pandas.core.dtypes.dtypes import register_extension_dtype from sqlalchemy i ...
- Struts2-从值栈获取list集合数据(三种方式)
创建User封装数据类 public class User { private String username; private String password; public String getP ...
- 使用pip管理库
2.5 使用pip管理库 安装Python后会默认安装pip工具,该工具可以用来安装.升级和移除库.默认情况下 pip 将从[Python Package Index]https://pypi.org ...
- HCIE笔记-第四节-MAC地址+网络层
mac地址 = 显示16进制 = 12个16进制数 二进制[逢2进1] 0/1 = 0/1 10=2 11=3 100=4 101=5 110=6 111=7 1000=8 1001=9 1010=1 ...
- xss攻击和防御
简介 XSS攻击全称跨站脚本攻击,是为不和层叠样式表(Cascading Style Sheets, CSS)的缩写混淆,故将跨站脚本攻击缩写为XSS,XSS是一种在web应用中的计算机安全漏洞,它允 ...
- zookeeper篇-zoo.cfg配置
点赞再看,养成习惯,微信搜索「小大白日志」关注这个搬砖人. 文章不定期同步公众号,还有各种一线大厂面试原题.我的学习系列笔记. zoo.cfg即/usr/local/java/zookeeper/co ...
- javaScript深拷贝和浅拷贝简单梳理
在了解深拷贝和浅拷贝之前,我们先梳理一下: JavaScript中,分为基本数据类型(原始值)和复杂类型(对象),同时它们各自的数据类型细分下又有好几种数据类型 基本数据类型 数字Number 字符串 ...