.

作者 | 磊哥

来源 | Java面试真题解析(ID:aimianshi666)

转载请联系授权(微信ID:GG_Stone)

单例模式的实现方法有很多种,如饿汉模式、懒汉模式、静态内部类和枚举等,当面试官问到“为什么单例模式一定要加 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() {@Overridepublic void run() {// 如果 flag 变量为 true 就终止执行while (!flag) {}System.out.println("终止执行");}});t1.start();// 1s 之后将 flag 变量的值修改为 trueThread t2 = new Thread(new Runnable() {@Overridepublic 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() {@Overridepublic void run() {// 如果 flag 变量为 true 就终止执行while (!flag) {}System.out.println("终止执行");}});t1.start();// 1s 之后将 flag 变量的值修改为 trueThread t2 = new Thread(new Runnable() {@Overridepublic 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 步:

  1. 创建内存空间。

  2. 在内存空间中初始化对象 Singleton。

  3. 将内存地址赋值给 instance 对象(执行了此步骤,instance 就不等于 null 了)。

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

总结

使用 volatile 可以解决内存可见性问题和防止指令重排序,我们在单例模式中使用 volatile 主要是使用 volatile 的后一个特性(防止指令重排序),从而避免多线程执行的情况下,因为指令重排序而导致某些线程得到一个未被完全实例化的对象,从而导致程序执行出错的情况。

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

公众号:Java面试真题解析

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

往期推荐

面试突击50:单例模式有几种写法?

面试突击49:说一下 JUC 中的 Exchange 交换器?

面试突击48:死锁的排查工具有哪些?

面试突击51:为什么单例一定要加 volatile?相关推荐

  1. 大叔手记(10):别再让面试官问你单例

    大叔手记(10):别再让面试官问你单例(暨6种实现方式让你堵住面试官的嘴) ... 2012-2-19 09:03| 发布者: benben| 查看: 283| 评论: 0 摘要: 引子经常从Recr ...

  2. Java面试常考之 单例设计模式(饿汉式单例、 懒汉式单例)

    所谓的单例设计指的是一个类只允许产生一个实例化对象. 对于单例设计模式也有两类形式:懒汉式.饿汉式. 饿汉式单例思想: 单例模式:表示任何类的对象有且只有一个. 首先控制对象的产生数量:将构造方法私有 ...

  3. swift -- 单例+ lazy懒加载 + 第三方库

    //工具类单例 static let goods : NHGoods = { let good = NHGoods() return good }() //懒加载 lazy var registerB ...

  4. 面试突击第一季完结:共 91 篇!

    感谢各位读者的支持与阅读,面试突击系列第一季到这里就要和大家说再见了. 希望所写内容对大家有帮助,也祝你们找到满意的工作. 青山不改,细水长流,我们下一季再见! 91:MD5 加密安全吗? 90:过滤 ...

  5. java饿汉式有啥作用,Java面试 - 什么是单例设计模式,为什么要使用单例设计模式,如何实现单例设计模式(饿汉式和懒汉式)?...

    什么是单例设计模式? 单例设计模式就是一种控制实例化对象个数的设计模式. 为什么要使用单例设计模式? 使用单例设计模式可以节省内存空间,提高性能.因为很多情况下,有些类是不需要重复产生对象的.如果重复 ...

  6. java面试突击-2022最新迭代redis\mq\springCloud-纯手打

    本博客是本人纯手打然后去网上百度的图片,转发请注明出处 按照自己的理解适合给初级程序员找工作用的 顺便给自己回顾一下,都是按照自己的理解来写的,有的地方不通顺或者不理解可以问我,有写不对的地方或者不同 ...

  7. mysql为什么要单例_为什么要用单例,你真的会写单例模式吗

    优秀的设计结构可以规避很多潜在的性能问题,对系统性能的影响可能远远大于代码的优化,所以我们需要知道一些设计模式和方法. 单例模式: 单例模式是一种对象创建模式,用于生产一个对象的实例,它可以确保系统中 ...

  8. 第 5 章 单例设计模式

    第 5 章 单例设计模式 1.单例设计模式介绍 所谓类的单例设计模式, 就是采取一定的方法保证在整个的软件系统中, 对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法(静态方法). ...

  9. Javascript 设计模式 单例

    转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/30490955 一直很喜欢Js,,,今天写一个Js的单例模式实现以及用法. 1.单 ...

最新文章

  1. android 释放so,在安卓项目里部署so文件你需要知道的知识
  2. 《Oracle高性能SQL引擎剖析:SQL优化与调优机制详解》一2.2 内部函数与操作
  3. 微软图表控件MsChart使用说明[转]
  4. android jni fork()子进程不运行_安卓系统最重要的进程之一:system_server详细分析...
  5. 需求用例分析之七:业务用例之小结
  6. mac版smali2java_Android反编译apk并重新打包签名(Mac环境)
  7. UnpooledHeadByteBuf源码分析
  8. winform 程序制作自己的数字签名(续)
  9. YouTube-DNN优化原理推导
  10. poi 读取excel
  11. python date,datetime 和time的区别
  12. Android AIDL远程服务demo
  13. 将像素图转换为矢量图
  14. chrome 扩展插件API
  15. 魔兽世界阿拉索人数最多服务器,魔兽世界8月国服人口普查 2019wow各服务器阵容比例汇总...
  16. SCI收录期刊——采矿和选矿
  17. Python下载新浪微博视频(流式下载)
  18. Windows系统下运行hadoop、HBase程序出错Could not locate executablenull\bin\winutils.exe in the Hadoop binaries
  19. Java数据结构学习——排序二叉树
  20. 计算机科学文献中英文对照,计算机科学和技术英文文献.doc

热门文章

  1. 浮点数强制转换整数,四舍五入
  2. Python|随机数的奥秘
  3. 读书笔记——吴翰清《白帽子讲Web安全》
  4. HTML点击图片实现跳转的两种方法
  5. 通过线性回归模型及优化实现AQI分析与预测
  6. android 获取视频长度,android中如何获取视频时长
  7. 如何学习微信公众平台开发?
  8. 微信域名拦截检测API接口
  9. kdj值应用口诀_kdj指标怎么用?KDJ值应用口诀(2)
  10. linux 主流浏览器,各主流浏览器(PC、移动端)userAgent属性信息介绍