当前位置: 首页 > 编程笔记 >

浅谈java 单例模式DCL的缺陷及单例的正确写法

闾丘选
2023-03-14
本文向大家介绍浅谈java 单例模式DCL的缺陷及单例的正确写法,包括了浅谈java 单例模式DCL的缺陷及单例的正确写法的使用技巧和注意事项,需要的朋友参考一下

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虚拟机上保证了单例,并且也是懒式加载。

以上这篇浅谈java 单例模式DCL的缺陷及单例的正确写法就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持小牛知识库。

 类似资料:
  • 本文向大家介绍浅谈Java编程中的单例设计模式,包括了浅谈Java编程中的单例设计模式的使用技巧和注意事项,需要的朋友参考一下 写软件的时候经常需要用到打印日志功能,可以帮助你调试和定位问题,项目上线后还可以帮助你分析数据。但是Java原生带有的System.out.println()方法却很少在真正的项目开发中使用,甚至像findbugs等代码检查工具还会认为使用System.out.print

  • 单例模式算是设计模式中最容易理解,也是最容易手写代码的模式了吧。但是其中的坑却不少,所以也常作为面试题来考。本文主要对几种单例写法的整理,并分析其优缺点。很多都是一些老生常谈的问题,但如果你不知道如何创建一个线程安全的单例,不知道什么是双检锁,那这篇文章可能会帮助到你。 懒汉式,线程不安全 当被问到要实现一个单例模式时,很多人的第一反应是写出如下的代码,包括教科书上也是这样教我们的。 public

  • 本文向大家介绍详解Java中如何正确书写单例模式,包括了详解Java中如何正确书写单例模式的使用技巧和注意事项,需要的朋友参考一下 单例模式算是设计模式中最容易理解,也是最容易手写代码的模式,但是其中涉及的知识点却一点也不少,所以经常作为面试题来考。一般单例都是五种写法:懒汉,饿汉,双重校验锁,静态内部类和枚举。为了记录学习过程的过程,这里整理了几种常见的单例写法, 青铜5:(Lazy-loade

  • 本文向大家介绍浅析php单例模式,包括了浅析php单例模式的使用技巧和注意事项,需要的朋友参考一下 本系列文章来总结一下设计模式在PHP中的应用,这是第一篇创建型模式之单例模式。 一、设计模式简介 首先我们来认识一下什么是设计模式: 设计模式是一套被反复使用、容易被他人理解的、可靠的代码设计经验的总结。 设计模式不是Java的专利,我们用面向对象的方法在PHP里也能很好的使用23种设计模式。 那么

  • 本文向大家介绍Java中单例模式的7种写法,包括了Java中单例模式的7种写法的使用技巧和注意事项,需要的朋友参考一下 第一种(懒汉,线程不安全): 这种写法lazy loading很明显,但是致命的是在多线程不能正常工作。 第二种(懒汉,线程安全): 这种写法能够在多线程中很好的工作,而且看起来它也具备很好的lazy loading,但是,遗憾的是,效率很低,99%情况下不需要同步。 第三种(饿

  • 本文向大家介绍浅谈C#单例模式的实现和性能对比,包括了浅谈C#单例模式的实现和性能对比的使用技巧和注意事项,需要的朋友参考一下 简介 单例指的是只能存在一个实例的类(在C#中,更准确的说法是在每个AppDomain之中只能存在一个实例的类,它是软件工程中使用最多的几种模式之一。在第一个使用者创建了这个类的实例之后,其后需要使用这个类的就只能使用之前创建的实例,无法再创建一个新的实例。通常情况下,单