1 单例模式

大家对单例模式并不会陌生,当创建一个对象需要消耗比较多资源时,例如创建数据库连接和消息服务端等等,这时我们选择只创建一份这种类型的对象并在进程内共享。

但是单例模式想要写好并不容易,我们写多个版本的单例模式看看每个版本都有什么问题。

1.1 版本一

这个版本问题非常明显:多个线程可能同时执行到语句1,而此时myConnection都为空,造成连接对象被多次创建。

public class MySimpleConnection {private static MySimpleConnection myConnection = null;private MySimpleConnection() {System.out.println(Thread.currentThread().getName() + " -> init connection");}public static MySimpleConnection getConnection() {if (null == myConnection) { // 语句1myConnection = new MySimpleConnection();}return myConnection;}public static void main(String[] args) {for (int i = 0; i < 10; i++) {new Thread(() -> {MySimpleConnection.getConnection();}, "threadName" + i).start();}}
}

执行结果可以看出连接被创建多次:

threadName1 -> init connection
threadName4 -> init connection
threadName3 -> init connection
threadName2 -> init connection
threadName0 -> init connection

1.2 版本二

这个版本在getConnection方法增加了同步关键字,可以正确处理同步问题,程序执行正确符合预期,但是将同步关键词加在方法上锁粒度较大,可能会影响性能。

public class MySynchronizeConnection {private static MySynchronizeConnection myConnection = null;private MySynchronizeConnection() {System.out.println(Thread.currentThread().getName() + " -> init connection");}public static synchronized MySynchronizeConnection getConnection() {if (null == myConnection) {myConnection = new MySynchronizeConnection();}return myConnection;}public static void main(String[] args) {for (int i = 0; i < 10; i++) {new Thread(() -> {MySynchronizeConnection.getConnection();}, "threadName" + i).start();}}
}

执行结果正确符合预期:

threadName0 -> init connection

1.3 版本三

这个版本采用DCL(Double Check lock)双重检查锁,缩小了同步锁粒度,性能会有所提升。

public class MyDCLConnection {private static MyDCLConnection myConnection = null;private MyDCLConnection() {System.out.println(Thread.currentThread().getName() + " -> init connection");}public static MyDCLConnection getConnection() {if (null == myConnection) {synchronized (MyDCLConnection.class) {if (null == myConnection) {myConnection = new MyDCLConnection();}}}return myConnection;}public static void main(String[] args) {for (int i = 0; i < 10; i++) {new Thread(() -> {MyDCLConnection.getConnection();}, "threadName" + i).start();}}
}

这段代码看似没有问题了,但是其实有一个严重问题:这段代码有可能引发空指针异常,也就是调用getConnection方法会拿到一个空对象。

你可能会说不对,我们不是判断了当连接不为空时才获取连接吗?怎么会拿到一个空对象呢?这就引出我们下一个话题:指令重排。

2 指令重排

在JVM编译代码时或者CPU执行JVM字节码时,为了提升性能可能对代码进行指令重排,也就是说代码执行顺序不一定是代码编写顺序。

指令重排目的是为了在不改变程序运行结果的前提下,优化程序运行效率,其中不改变运行结果是指在单线程场景下。

我们分析一个指令重排实例。

public void test() {int a = 1; // 语句1int b = 2; // 语句2a = a + 1; // 语句3b = b * 2; // 语句4
}

这段代码执行顺序可能如下:

1234
1243
1324
2134
2143
2413

我们思考一下语句3和语句4会不会第一个执行?答案是不会。在进行指令重排时必须要考虑数据依赖性。

语句3依赖语句1,语句4依赖语句2,所以语句3和语句4不会第一个执行。这也告诉我们如果语句之间没有依赖关系就可能发生指令重排。

指令重排在多线程场景下会产生什么问题呢?我们分析一个多线程指令重排实例。

public class MyTest {int a = 0;boolean b = false;public void method1() {a = 1000; // 语句1b = true; // 语句2}public void method2() {if (b) {a = a + 1; // 语句3System.out.println(a);}}public static void main(String[] args) {for (int i = 0; i < 10000; i++) {MyTest test = new MyTest();new Thread(() -> test.method1()).start();new Thread(() -> test.method2()).start();}}
}

我们思考一下a最终输出值是多少?答案是有可能是1或者1001。

  • 由于变量a和b不存在数据依赖关系,所以经过指令重排,语句2可能先于语句1执行

  • 执行完语句2后可能还没有执行语句1,method2抢到执行机会执行语句3,这时执行结果等于1

  • 如果指令不重排,执行结果等于1001

所以在多线程环境,运行结果具有不确定性是指令重排可能带来的问题。

3 回到问题

再回到第一章节的问题:为什么会出现空指针异常?我们分析这一段代码。

public static MyDCLConnection getConnection() {if (null == myConnection) { // 语句1synchronized (MyDCLConnection.class) {if (null == myConnection) {myConnection = new MyDCLConnection(); // 语句2}}}return myConnection; // 语句3
}

我们重点分析语句2,new操作在更细的层面分为以下三个步骤:

(A) 分配新对象内存
(B) 调用类构造器初始化成员变量
(C) instance被赋为指向新对象的引用

经过指令重排可能形成以下新顺序:

(A) 分配新对象内存
(B) instance被赋为指向新对象的引用
(C) 调用类构造器初始化成员变量

根据新顺序我们分析一种异常场景:

  • 线程1执行到语句2,执行到instance被赋为指向新对象引用这个步骤,还没有进行初始化对象

  • 此时线程2执行到语句1,由于instance已经被赋为指向新对象的引用,myConnection已经不等于null,所以可以执行到语句3

  • 但是语句3返回的是没有进行初始化的对象,所以使用这个对象就会抛出空指针异常

4 Volatile

上述问题应该如何解决呢?本章节我们来谈一谈Volatile关键字。Volatile是JVM提供的轻量级同步机制,具有以下特性:

  • 保证可见性

  • 不保证原子性

  • 保证有序性(禁止指令重排)

其中保证可见性和不保证原子性不在本文进行讨论,本文我们分析Volatile如何保证有序性。

Volatile禁止指令重排原理是使用了内存屏障。内存屏障是一种CPU指令,它使CPU或者编译器对屏障指令之前和之后发出的内存操作执行一个排序约束。通过插入内存屏障指令,禁止在内存屏障指令前后的指令进行重排序。内存屏障有如下四种类型:

  • LoadLoad

  • StoreStore

  • LoadStore

  • StoreLoad

这样说有一些抽象,我们结合代码进行分析,还是使用上文代码实例,只是不同的是这一次我们新增了Volatile关键字。

public class MyTest {int a = 0;volatile boolean b = false;public void method1() {a = 1000; // 语句1b = true; // 语句2}public void method2() {if (b) {a = a + 1; // 语句3System.out.println(a);}}public static void main(String[] args) {for (int i = 0; i < 10000; i++) {MyTest test = new MyTest();new Thread(() -> test.method1()).start();new Thread(() -> test.method2()).start();}}
}

我们将b变量声明为Volatile,那么在为b赋值即Volatile写前后会加上如下屏障,从而保证了语句1和语句2执行顺序不会重排。

volatile boolean b = false;
public void method1() {a = 1000; // 语句1StoreStore屏障b = true; // 语句2StoreLoad屏障
}

在JDK5之后Volatile还可以保证对象构造是有序的,也就是说new操作如下步骤可以保证有序,这就为我们解决DCL空指针异常提供了思路。

(A) 分配新对象内存
(B) 调用类构造器初始化成员变量
(C) instance被赋为指向新对象的引用

5 解决方案

经过上述分析我们可以使用Volatile解决单例DCL空指针异常。

public class MyVolatileConnection {private static volatile MyVolatileConnection myConnection = null;private MyVolatileConnection() {System.out.println(Thread.currentThread().getName() + " -> init connection");}public static MyVolatileConnection getConnection() {if (null == myConnection) {synchronized (MyVolatileConnection.class) {if (null == myConnection) {myConnection = new MyVolatileConnection();}}}return myConnection;}public static void main(String[] args) {for (int i = 0; i < 10; i++) {new Thread(() -> {MyVolatileConnection.getConnection();}, "threadName" + i).start();}}
}

代码改动并不大只需在声明MyConnection变量处加上Volatile关键字。

本文我们从单例模式的一个问题出发,一步步分析到Volatile关键字原理并最终解决单例模式DCL空指针问题,希望本文对大家有所帮助。

特别推荐一个分享架构+算法的优质内容,还没关注的小伙伴,可以长按关注一下:长按订阅更多精彩▼如有收获,点个在看,诚挚感谢

面试官:你写的单例模式有空指针异常,请你用Volatile改一下。我愣了五分钟...相关推荐

  1. Chat Top10 | 给面试官手写一个 Nacos,多少 K?

    每周推荐的最新 Chat Top10 没有固定主题,仅仅是编辑部参考多方评分和反馈挑选出来的好文章,不一定适合你的口味,建议小心食用- 我们一起看下第三期 Chat Top10 都有哪些内容 ???? ...

  2. 面试官再问你优先级队列,请把这篇文章丢给他

    程序员常用的IDEA插件:https://github.com/silently9527/ToolsetIdeaPlugin 完全开源的淘客项目:https://github.com/silently ...

  3. 面试官问一个数据表字段怎么表示多种业务含义?我愣了五分钟

    1 需求背景 在系统中用户一共有三种角色:普通用户,管理员,超级管理员,现在需要设计一张用户角色表记录这类信息.我们不难设计出如下方案. id name super admin normal 101 ...

  4. 我向面试官讲解了单例模式,他对我竖起了大拇指

    作者:小菠萝 单例模式相信大家都有所听闻,甚至也写过不少了,在面试中也是考得最多的其中一个设计模式,面试官常常会要求写出两种类型的单例模式并且解释其原理,废话不多说,我们开始学习如何很好地回答这一道面 ...

  5. 我给面试官讲解了单例模式后,他对我竖起了大拇指!

    单例模式相信大家都有所听闻,甚至也写过不少了,在面试中也是考得最多的其中一个设计模式,面试官常常会要求写出两种类型的单例模式并且解释其原理,废话不多说,我们开始学习如何很好地回答这一道面试题吧. 1. ...

  6. 动画:如何给面试官写一个满意的冒泡排序

    作者 | 小鹿 来源 | 小鹿动画学编程 写在前边 对于冒泡排序,很多小伙伴已经可以说很熟悉了,顺手就可以写出来,但对于一个初学者来说,小鹿想通过这篇文章,让你一次性就理解冒泡排序以及冒泡排序的优化, ...

  7. 面试官告诉你,为什么一定要过手写算法这一关

    IT互联网公司的技术面试,一般都会有手写算法的这一关,有的简单,有的复杂,根据岗位的不同有所差异.面向业务的研发岗,算法要求不高,考察的算法不会太难.面向算法一类的研发岗,算法就比较难了.我主要关注的 ...

  8. 关于MySQL的酸与MVCC和面试官小战三十回合

    此刻,正坐在办公室里等待面试,心情xue微有点忐忑,不知道待会儿老面试官经不经得住我的折磨. 只见一抹光亮闪过,面试官推门而入,我抬头望去,强者的气息铺面而来,没错是那味儿. 看到面试官头上那&quo ...

  9. 程序员过关斩将--面试官再问你Http请求过程,怼回去!

    Http介绍 超文本传输协议(HTTP,HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络协议.所有的WWW文件都必须遵守这个标准.设计HTTP最初的目的是为了提 ...

最新文章

  1. docwizard c++程序文档自动生成工具_如何开发一个基于 TypeScript 的工具库并自动生成文档
  2. XCTF WEB webshell
  3. 最终的动画函数封装(2)
  4. 学习笔记02:直播串讲02
  5. 【51单片机快速入门指南】2.2:任意位/任意长度数码管显示数字、小数
  6. 计算机数学基础知识点归纳,《计算机数学基础》(一)――离散数学期末复习参考...
  7. java8(1)--- lambda
  8. sql server 开源_开源工具SQL Server安全注意事项
  9. python空格算一个字符吗_举例说明python中空格是属于字符
  10. com.microsoft.sqlserver.jdbc.SQLServerException: Socket closed 或者 该连接已关闭
  11. 最近使用VirtualBox安装虚拟机,频繁崩溃。是不是有什么隐藏限制?
  12. android内置so库,带so库的apk正确内置到system/app详解
  13. Android运行报错:Error: Static interface methods are only supported starting with Android N
  14. 2021年焊工(初级)模拟考试及焊工(初级)作业考试题库
  15. 适用于 Windows 10/11 电脑 的 5 大好用的离线录屏软件
  16. javascript交互性设计
  17. 20-HTML与HTML5常用标签(前端)
  18. 《python程序语言设计》第2章第15题几何正六边形面积。用def和class来完成
  19. vue2之v-for详解
  20. 微商在微信营销的时候微信封号的原因是什么?

热门文章

  1. 甘肃省计算机二级考试题库,2011甘肃省计算机等级考试二级最新考试试题库(完整版)...
  2. 单调栈 or 线段树扫描线 ---- E. Delete a Segment [单调栈+二分] [扫描线处理空白位置的技巧乘2]
  3. 牛客挑战赛36 D. 排名估算( “概率论全家桶”,好题,拉格朗日插值求自然数 k 次幂之和)
  4. CF510D Fox And Jumping(动态规划转换为最短路,O(n^2×2^9) -> O(nlogn),裴蜀定理应用)
  5. (2017)第八届蓝桥杯大赛个人赛省赛(软件类) C/C++ 大学A组 题解(第八题包子凑数)
  6. 解表化饮什么意思_为什么有人动不动就一身汗,有人再热也不出汗?中医告诉真实原因...
  7. 中双目运算符_C++日志(四十)教你如何以非成员函数的形式重载运算符
  8. python requests 重定向_认识Python最最最常用语重要的库Requests
  9. python量化外汇交易_用Python实现一个Dual Thrust数字货币量化交易策略
  10. wamp php非线程安全,wampserver PHP多版本切换