Java Volatile应用单例模式实现过程解析

 更新时间:2020年11月04日 10:38:17   作者:柒  
这篇文章主要介绍了Java Volatile应用单例模式实现过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下

单例模式

回顾一下,单线程下的单例模式代码

饿汉式

  • 构造器私有化
  • 自行创建,并且用静态变量保存static
  • 向外提供这个实例 public
  • 强调这是一个单例,用final
public class sington(){
  public final static INSTANCE = new singleton();
  private singleton(){}
}

第二种:jdk1.5之后用枚举类型

枚举类型:表示该类型的对象是有限的几个

我们可以限定为1个,就称了单例

public enum Singleto{
  INSTANCE
}

第三种静态代码块

public class Singleton{
public final static INSTANCE;
static{
  INSTANCE = new Singleton();
}
private Singleton(){}

}

懒汉式构造器私有化

用一个静态变量保存这个唯一实例

提供一个静态方法,获取这个实例

public class Singleton{
  private static Singleton INSTANCE;
  private Singleton(){}
  public static Singleton getInstance(){
    if(instance==null){
      INSTANCE = new Singleton();
    }
    return INSTANCE;
  }
}
public class SingletonDemo {

  private static SingletonDemo instance = null;

  private SingletonDemo () {
    System.out.println(Thread.currentThread().getName() + "\t 我是构造方法SingletonDemo");
  }

  public static SingletonDemo getInstance() {
    if(instance == null) {
      instance = new SingletonDemo();
    }
    return instance;
  }

  public static void main(String[] args) {
    // 这里的 == 是比较内存地址
    System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
    System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
    System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
    System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
  }
}

最后输出结果:


但是在多线程的环境下,我们的单例模式是否还是同一个对象了

public class SingletonDemo {

  private static SingletonDemo instance = null;

  private SingletonDemo () {
    System.out.println(Thread.currentThread().getName() + "\t 我是构造方法SingletonDemo");
  }

  public static SingletonDemo getInstance() {
    if(instance == null) {
      instance = new SingletonDemo();
    }
    return instance;
  }

  public static void main(String[] args) {
    for (int i = 0; i < 10; i++) {
      new Thread(() -> {
        SingletonDemo.getInstance();
      }, String.valueOf(i)).start();
    }
  }
}

从下面的结果我们可以看出,我们通过SingletonDemo.getInstance() 获取到的对象,并不是同一个,而是被下面几个线程都进行了创建,那么在多线程环境下,单例模式如何保证呢?

解决方法一

引入synchronized关键字

public synchronized static SingletonDemo getInstance() {
  if(instance == null) {
    instance = new SingletonDemo();
  }
  return instance;
}

输出结果:


我们能够发现,通过引入Synchronized关键字,能够解决高并发环境下的单例模式问题

但是synchronized属于重量级的同步机制,它只允许一个线程同时访问获取实例的方法,但是为了保证数据一致性,而减低了并发性,因此采用的比较少

解决方法二

通过引入DCL Double Check Lock双端检锁机制

  public static SingletonDemo getInstance() {
    if(instance == null) {
      // 同步代码段的时候,进行检测
      synchronized (SingletonDemo.class) {
        if(instance == null) {
          instance = new SingletonDemo();
        }
      }
    }
    return instance;
  }

最后输出的结果为:

从输出结果来看,确实能够保证单例模式的正确性,但是上面的方法还是存在问题的

DCL(双端检锁)机制不一定是线程安全的,原因是有指令重排的存在,加入volatile可以禁止指令重排

原因是在某一个线程执行到第一次检测的时候,读取到 instance 不为null,instance的引用对象可能没有完成实例化。因为 instance = new SingletonDemo();可以分为以下三步进行完成:

  • memory = allocate(); // 1、分配对象内存空间
  • instance(memory); // 2、初始化对象
  • instance = memory; // 3、设置instance指向刚刚分配的内存地址,此时instance != null

但是我们通过上面的三个步骤,能够发现,步骤2 和 步骤3之间不存在 数据依赖关系,而且无论重排前 还是重排后,程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。

  • memory = allocate(); // 1、分配对象内存空间
  • instance = memory; // 3、设置instance指向刚刚分配的内存地址,此时instance != null,但是对象还没有初始化完成
  • instance(memory); // 2、初始化对象

这样就会造成什么问题呢?

也就是当我们执行到重排后的步骤2,试图获取instance的时候,会得到null,因为对象的初始化还没有完成,而是在重排后的步骤3才完成,因此执行单例模式的代码时候,就会重新在创建一个instance实例

指令重排只会保证串行语义的执行一致性(单线程),但并不会关系多线程间的语义一致性

所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,这就造成了线程安全的问题

所以需要引入volatile,来保证出现指令重排的问题,从而保证单例模式的线程安全性

private static volatile SingletonDemo instance = null;

最终代码

public class SingletonDemo {

  private static volatile SingletonDemo instance = null;

  private SingletonDemo () {
    System.out.println(Thread.currentThread().getName() + "\t 我是构造方法SingletonDemo");
  }

  public static SingletonDemo getInstance() {
    if(instance == null) {
      // a 双重检查加锁多线程情况下会出现某个线程虽然这里已经为空,但是另外一个线程已经执行到d处
      synchronized (SingletonDemo.class) //b
      { 
      //c不加volitale关键字的话有可能会出现尚未完全初始化就获取到的情况。原因是内存模型允许无序写入
        if(instance == null) { 
        	// d 此时才开始初始化
          instance = new SingletonDemo();
        }
      }
    }
    return instance;
  }

  public static void main(String[] args) {
//    // 这里的 == 是比较内存地址
//    System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
//    System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
//    System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
//    System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());

    for (int i = 0; i < 10; i++) {
      new Thread(() -> {
        SingletonDemo.getInstance();
      }, String.valueOf(i)).start();
    }
  }
}

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

相关文章

  • SpringBoot中注册过滤器的几种实现方式

    SpringBoot中注册过滤器的几种实现方式

    本文主要介绍了SpringBoot中注册过滤器的几种实现方式,主要介绍了三种方式,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2024-01-01
  • 浅谈Maven环境隔离应用

    浅谈Maven环境隔离应用

    这篇文章主要介绍了浅谈Maven环境隔离应用,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-09-09
  • 在idea环境下构建springCloud项目

    在idea环境下构建springCloud项目

    本篇文章主要介绍了在idea环境下构建springCloud项目,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-11-11
  • Java开发实现的Socket双向通信功能示例

    Java开发实现的Socket双向通信功能示例

    这篇文章主要介绍了Java开发实现的Socket双向通信功能,结合实例形式分析了java基于socket实现的服务器端与客户端双向通信相关操作技巧,需要的朋友可以参考下
    2018-01-01
  • Java 精炼解读时间复杂度与空间复杂度

    Java 精炼解读时间复杂度与空间复杂度

    对于一个算法,其时间复杂度和空间复杂度往往是相互影响的,当追求一个较好的时间复杂度时,可能会使空间复杂度的性能变差,即可能导致占用较多的存储空间,这篇文章主要给大家介绍了关于Java时间复杂度、空间复杂度的相关资料,需要的朋友可以参考下
    2022-03-03
  • SpringMVC实现前端后台交互传递数据

    SpringMVC实现前端后台交互传递数据

    本篇文章主要介绍了SpringMVC实现前端后台传递数据的方法,具有一定的参考价值,感兴趣的小伙伴们可以参考一下。
    2017-03-03
  • SpringBoot中的自定义Banner详细解析

    SpringBoot中的自定义Banner详细解析

    这篇文章主要介绍了SpringBoot中的自定义Banner详细解析,Banner即横幅标语,我们在启动SpringBoot项目时会将Banner信息打印至控制台,我们可以输出一些图形、SpringBoot版本信息等内容,需要的朋友可以参考下
    2024-01-01
  • java读取resource目录下文件的方法示例

    java读取resource目录下文件的方法示例

    这篇文章主要介绍了利用java读取resource目录下文件的方法,文中给出了详细的示例代码,相信对大家具有一定的参考借鉴,需要的朋友们下面来一起看看吧。
    2017-02-02
  • Java黑科技:replace首个替换一秒搞定

    Java黑科技:replace首个替换一秒搞定

    要实现只替换第一个匹配项,可以使用Java中的String类的replaceFirst方法,该方法接受两个参数,第一个参数是要替换的字符串或正则表达式,第二个参数是替换后的字符串,需要的朋友可以参考下
    2023-10-10
  • 如何利用grep-console插件使Intellij idea显示多颜色调试日志

    如何利用grep-console插件使Intellij idea显示多颜色调试日志

    这篇文章主要介绍了利用grep-console插件使Intellij idea显示多颜色调试日志,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-05-05

最新评论