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

.a30322568ca622050809596a22033841.png

作者 | 磊哥

来源 | 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();
}

以上程序的执行结果如下:c0682cd1209e2141db3ee9b495bac562.png然而,以上程序执行了 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();}
}

以上程序的执行结果如下:411c1f873d77080eba78fbc11e412345.png从上述执行结果我们可以看出,使用 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

017e29dd1d1154930d1c98628dab0c3c.gif

往期推荐

87e1c5fa7abe7b83dd457432b426cfd8.png

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


a1580865baceddfc4326f9cff4d0d599.png

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


c6708de069f3c9b9a27973bb3800ced0.png

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


89fe76f38b6c8a461c2f53f71b7b6c2d.gif

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/544136.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

kotlin 扩展类的功能_Kotlin程序| 扩展功能功能

kotlin 扩展类的功能扩展功能 (Extension function) Kotlin provides the ability to add more functionality to the existing class without inheriting them. Kotlin可以在不继承现有类的情况下为其添加更多功能。 This is done via a special declaration called "Ext…

聊聊保证线程安全的10个小技巧

前言对于从事后端开发的同学来说,线程安全问题是我们每天都需要考虑的问题。线程安全问题通俗的讲:主要是在多线程的环境下,不同线程同时读和写公共资源(临界资源),导致的数据异常问题。比如:变…

Raid控制器

转载于:https://blog.51cto.com/xuepengdou/1699799

并行计算机架构_计算机科学组织| 并行处理

并行计算机架构并行处理 (Parallel Processing) Parallel processing is processing of the data concurrently. We process the data concurrently to fulfill the demands of the increasingly high performance so that to achieve better throughput instead of processing…

15个必知的Mysql索引失效场景,别再踩坑了!

背景 无论你是技术大佬,还是刚入行的小白,时不时都会踩到Mysql数据库不走索引的坑。常见的现象就是:明明在字段上添加了索引,但却并未生效。前些天就遇到一个稍微特殊的场景,同一条SQL语句,在某些参数下生效…

网站高可用数据

网站高可用数据1、CAP原理:数据一致性,数据可用性,分区耐受性,无法同时满足2、数据备份a、冷备份b、热备份1、同步热备份2、异步热备份3、失效转移a、失效确认b、访问转移c、数据恢复转载于:https://blog.51cto.com/hanchengen/17…

运维工程师常见软件故障_软件故障 软件工程师

运维工程师常见软件故障软件故障 (Software Failure) A failure that shows up when the user recognizes that the software has come to bring to an end to deliver the anticipated result with respect to the specification input values. The user may need to identify…

干掉 Swagger UI,这款神器更好用、更高效!

事情是这样的:今天我们公司的后端说他接口写完了,并分享了一个接口文档给我。用的就是 Swagger UI 自动生成的那种接口文档,就像这种:这种 Swagger UI文档我每次看着就头大,毛病多多查看多级模型时要一级级点开在接口数…

Android UI ActionBar功能-ActionBarProvider的使用

分享功能是很多App都有一个功能,ActionBarProvider可以实现分享功能: 3.0以前的版 本和3.0以后的版 本的区别: public class MainActivity extends Activity {private ShareActionProvider provider;Overrideprotected void onCreate(Bundle …

ruby 类方法与实例方法_Ruby Set相交? 实例方法

ruby 类方法与实例方法Ruby Set相交? 方法 (Ruby Set intersect? Method) intersect?(Set) method is a method which is predefined in Rubys library. You may call this method as an exact opposite of Set.disjoint?() method. With the help of this metho…

面渣逆袭:MyBatis连环20问,这谁顶得住?

大家好,今天我们的主角是MyBatis,作为当前国内最流行的ORM框架,是我们这些crud选手最趁手的工具,赶紧来看看面试都会问哪些问题吧。基础1.说说什么是MyBatis?MyBatis logo先吹一下:Mybatis 是一个半 ORM(对…

Wi-Fi 协议和数率?

IEEE 802.11Wi-Fi 协议摘要 协议 频率 信号 最大数据速率 传统 802.11 2.4GHz FHSS 或 DSSS 2Mbps 802.11A 5GHz OFDM 54Mbps 802.11B 2.4GHz 人力资源DSSS 11Mbps 802.11G 2.4GHz OFDM 54Mbps 802.11n 2.4 或 5GHz OFDM 600Mbps(理论值) 802.11AC 5…

as_hash ruby_Ruby中带有示例的Hash.keep_if方法

as_hash rubyHash.keep_if方法 (Hash.keep_if Method) In this article, we will study about Hash.keep_if Method. The working of this method can be predicted with the help of its name but it is not as simple as it seems. Well, we will understand this method wit…

高并发下如何防重?

前言最近测试给我提了一个bug,说我之前提供的一个批量复制商品的接口,产生了重复的商品数据。追查原因之后发现,这个事情没想象中简单,可以说一波多折。1. 需求产品有个需求:用户选择一些品牌,点击确定按钮…

Virtualbox中win7虚拟机中U盘不可用问题的解决

Virtualbox版本是5.0.0,主机运行多是Ubuntu12.04 LTS,虚拟机是Win7 X64。起初Win7正常运行,Virtualbox的增强功能已安装。下面是如何一步一步解决U盘不可用的详细过程。 1. 直接插入U盘,Win7下无任何反映 Virtualbox管理器中&…

面试突击55:delete、drop、truncate有什么区别?

作者 | 磊哥来源 | Java面试真题解析(ID:aimianshi666)转载请联系授权(微信ID:GG_Stone)在 MySQL 中,删除的方法总共有 3 种:delete、truncate、drop,而三者的用法和使用…

python中八进制_在Python中以八进制格式输入数字

python中八进制Syntax to convert octal value to an integer (decimal format), 将八进制值转换为整数(十进制格式)的语法, int(oct_value, 8)Here, 这里, oct_value should contain the valid octal value oct_value应该包含有效的八进制值 8 is the…

Ruby file操作cheatsheet

每次都要查,真是蛋疼,不如一次性总结一下,以后再不记得就来这里找好了。以下代码中需要用到的文件名:filename ‘testfile.txt’ 读取其中的全部内容:File.read(filename)将一个字符串一次性写入这个文件:…

大厂也在用的 6种 数据脱敏方案,别做泄密内鬼

最近连着几天晚上在家总是接到一些奇奇怪怪的电话,“哥,你是 xxx 吧,我们这里是 xxx 高端男士私人会所...”,握草,我先是一愣,然后狠狠的骂了回去。一脸傲娇的转过头,面带微笑稍显谄媚&#xff…

在Python中使用OpenCV裁剪图像

What is Cropping? 什么是播种? Cropping is the removal of unwanted outer areas from a photographic or illustrated image. The process usually consists of the removal of some of the peripheral areas of an image to remove extraneous trash from the…