浅谈java 单例模式DCL的缺陷及单例的正确写法
作者:带你装逼带你飞的程序猿 发布时间:2022-04-14 05:58:23
1 前言
单例模式是我们经常使用的一种模式,一般来说很多资料都建议我们写成如下的模式:
/**
* Created by qiyei2015 on 2017/5/13.
*/
public class Instance {
private String str = "";
private int a = 0;
private static Instance ins = null;
/**
* 构造方法私有化
*/
private Instance(){
str = "hello";
a = 20;
}
/**
* DCL方式获取单例
* @return
*/
public static Instance getInstance(){
if (ins == null){
synchronized (Instance.class){
if (ins == null){
ins = new Instance();
}
}
}
return ins;
}
}
但是这种方式其实是有缺陷的,具体什么缺陷呢?我们首先要了解JVM了内存模型,请看下面分析
2 JVM内存模型
JVM模型如下图:
这里着重介绍下VM Stack,其他的我相信都比较熟悉。
VM Stack是线程私有的区域。他是java方法执行时的字典:它里面记录了局部变量表、 操作数栈、 动态链接、 方法出口等信息。
在《java虚拟机规范》一书中对这部分的描述如下:
栈帧( Frame)是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接 (Dynamic Linking)、 方法返回值和异常分派( Dispatch Exception)。
栈帧随着方法调用而创建,随着方法结束而销毁——无论方法是正常完成还是异常完成(抛出了在方法内未被捕获的异常)都算作方法结束。
栈帧的存储空间分配在 Java 虚拟机栈( §2.5.5)之中,每一个栈帧都有自己的局部变量表( Local Variables, §2.6.1)、操作数栈( OperandStack, §2.6.2)和指向当前方法所属的类的运行时常量池( §2.5.5)的引用。
java中某个线程在访问堆中的线程共享变量时,为了加快访问速度,提升效率,会把该变量临时拷贝一份到自己的VM Stack中,并保持和堆中数据的同步。
3 传统DCL方式的缺陷
有了以上的基础知识我们就可以知道DCL方式的缺陷在哪儿了。当线程A在获取了Instance.class锁时,对ins进行 ins = new Instance() 初始化时,由于这是很多条指令,jvm可能会乱序执行。
这个时候如果线程B在执行if (ins == null)时,正常情况下,如果为true,说明需要获取Instance.class锁,等待初始化。
但是这时候,假设线程A再没有对ins进行初始化完,比如只对str进行了赋值,还没有来的及对a进行赋值,假如jvm将未完成赋值的值拷贝回堆中,这个时候线程B有可能读到的值就不是为null了,就会造成数据丢失的情况。这时候我们发现线程B获取的对象中a的值是0,而不是20
因为:对ins的写操作不 happen-before 对它的读操作
这就是DCL方式的缺陷,那么怎么避免呢?首先我们需要了解分析多线程的一大利器
4 happen-before原则
Happen-Before规则:
1 同一个线程中,书写在前面的操作happen-before书写在后面的操作。这条规则是说,在单线程 中操作间happen-before关系完全是由源代码的顺序决定的,这里的前提“在同一个线程中”是很重要的,这条规则也称为单线程规则 。
这个规则多少说得有些简单了,考虑到控制结构和循环结构,书写在后面的操作可能happen-before书写在前面的操作,不过我想读者应该明白我的意思。
2 对锁的unlock操作happen-before后续的对同一个锁的lock操作。这里的“后续”指的是时间上的先后关系,unlock操作发生在退出同步块之后,lock操作发生在进入同步块之前。这是条最关键性的规则,线程安全性主要依赖于这条规则。
但是仅仅是这条规则仍然不起任何作用,它必须和下面这条规则联合起来使用才显得意义重大。这里关键条件是必须对“同一个锁”的lock和unlock。
如果操作A happen-before操作B,操作B happen-before操作C,那么操作A happen-before操作C。这条规则也称为传递规
3 对volatile字段的写操作happen-before后续的对同一个字段的读操作.(Java5 新增)
4 单例模式的正确写法
有了以上的分析我们知道,我们只需要在保证对ins的访问是读在写之后即可,因此正确的做法是在ins 前加上一个关键字volatile。因此DCL的正确写法应该如下:
/**
* Created by qiyei2015 on 2017/5/13.
*/
public class Instance {
private String str = "";
private int a = 0;
private volatile static Instance ins = null;
/**
* 构造方法私有化
*/
private Instance(){
str = "hello";
a = 20;
}
/**
* DCL方式获取单例
* @return
*/
public static Instance getInstance(){
if (ins == null){
synchronized (Instance.class){
if (ins == null){
ins = new Instance();
}
}
}
return ins;
}
}
其实单例模式也有另一种我很喜欢的写法,那就是内部类:
/**
* Created by qiyei2015 on 2017/5/13.
*/
public class Instance {
/**
* 构造方法私有化
*/
private Instance(){
}
private static class SingleHolder{
private static final Instance ins = new Instance();
}
/**
* 内部类方式获取单例
* @return
*/
public static Instance getInstance(){
return SingleHolder.ins;
}
}
这种从jvm虚拟机上保证了单例,并且也是懒式加载。
来源:https://blog.csdn.net/qiyei2009/article/details/71813069
猜你喜欢
- 树的同构备忘!定义:给定两棵树r1、r2,如果r1可以通过若干次的左子树和右子树互换,使之与r2完全相同,这说明两者同构。举例树的构造树可以
- 项目分为前台和后台,前台主要为学生角色、后台主要为管理员角色。管理员添加试题和发布试卷,学生负责在线考试、在线查看成绩和错题记录列表等。管理
- 在web开发中,我们可能会有这样的需求,为了便于前台的JS的处理,我们需要将查询出的数据源格式比如:List<T>、DataTa
- Java 序列化和反序列化实例详解在分布式应用中,对象只有经过序列化才能在各个分布式组件之间传输,这就涉及到两个方面的技术-发送者将对象序列
- Spring depends-on的使用通过在XML中的<bean>里配置depends-on属性或者在一个类上使用注解@Dep
- 什么是JSON?JSON (JavaScript Object Notation) is a lightweight data-interc
- 一、MyBatis的增删改查1.1、新增<!--int insertUser();--><insert id="
- 一,Maven 依赖 pom.xml配置1, 去掉默认日志,以便切换到log4j2的日志依赖2, 然后添加如下两个日志依赖二,在工程根目录下
- 我们使用Jmeter测试同学的网站时,就会出现网站无法访问,403等错误。An error occurred.Sorry, the page
- Java类加载全过程一个java文件从被加载到被卸载这个生命过程,总共要经历4个阶段:加载->链接(验证+准备+解析)->初始化
- 我就废话不多说了,大家还是直接看代码吧~ @Test void testJava8ForeachMap() { Map<String,
- 持久层的那些事什么是 JDBCJDBC(JavaDataBase Connectivity)就是 Java 数据库连接, 说的直白点就是 使
- 在进行C#程序设计时,用的最多的莫过于string了,但有些时候由于不仔细或者基础的不牢固等因素容易出错,今天本文就来较为详细的总结一下C#
- 一、实现流程1.注册2.登录3.登录保持【状态续签】二、实现方法项目结构1.引入依赖<!-- spring-web --><
- 目录一、前言二、正文2.1 注解2.1.1 注解1:@Target({ElementType.TYPE})2.1.2 注解2:@Retent
- 介绍Spring Profiles 提供了一套隔离应用配置的方式,不同的 profiles 提供不同组合的配置,在不同的环境中,应用在启动时
- tcp客户端示例#include <errno.h> #include <sys/socket.h> #includ
- 面试题1:说一下抽象类和接口有哪些区别?正经回答:抽象类和接口的主要区别:从设计层面来说,抽象类是对类的抽象,是一种模板设计;接口是行为的抽
- 今天本文与大家分享如何得到数组中的最大值和最小值的实例。很适合Java初学者复习数组的基本用法与流程控制语句的使用。具体如下:这个程序主要是
- class MyThreadScopeData { // 单例 &nbs