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

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

(0)

相关推荐

  • 深入了解Java中Volatile关键字

    一.基本概念 先补充一下概念:Java 内存模型中的可见性.原子性和有序性. 可见性: 可见性是一种复杂的属性,因为可见性中的错误总是会违背我们的直觉.通常,我们无法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚至是根本不可能的事情.为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制. 可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的.也就是一个线程修改的结果.另一个线程马上就能看到.比如:用volatile修饰的变量,就会具有可见性.volatile修饰

  • 详细分析java并发之volatile关键字

    Java面试中经常会涉及关于volatile的问题.本文梳理下volatile关键知识点. volatile字意为"易失性",在Java中用做修饰对象变量.它不是Java特有,在C,C++,C#等编程语言也存在,只是在其它编程语言中使用有所差异,但总体语义一致.比如使用volatile 能阻止编译器对变量的读写优化.简单说,如果一个变量被修饰为volatile,相当于告诉系统说我容易变化,编译器你不要随便优化(重排序,缓存)我. Happens-before 规范上,Java内存模型遵

  • Java的Volatile实例用法及讲解

    在原子性.可见性.有序性中,volatile关键字主要在可见性中发挥作用. volatile声明的变量对所有线程来说是可见的,就是说当变量的值发生改变的时候,其他线程可以立马发现这个变化. public class Main { private static boolean isRuning; private static int number; private static class ReaderThread extends Thread { public void run() { whil

  • Java多线程之volatile关键字及内存屏障实例解析

    前面一篇文章在介绍Java内存模型的三大特性(原子性.可见性.有序性)时,在可见性和有序性中都提到了volatile关键字,那这篇文章就来介绍volatile关键字的内存语义以及实现其特性的内存屏障. volatile是JVM提供的一种最轻量级的同步机制,因为Java内存模型为volatile定义特殊的访问规则,使其可以实现Java内存模型中的两大特性:可见性和有序性.正因为volatile关键字具有这两大特性,所以我们可以使用volatile关键字解决多线程中的某些同步问题. volatile

  • Java volatile的适用场景实例详解

    把代码块声明为 synchronized,有两个重要后果,通常是指该代码具有 原子性(atomicity)和 可见性(visibility). 原子性意味着个时刻,只有一个线程能够执行一段代码,这段代码通过一个monitor object保护.从而防止多个线程在更新共享状态时相互冲突. 可见性则更为微妙,它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的. -- 如果没有同步机制提供的这种可见性保证,线程看到的共享变量可能是修改前的值或不一致的值,这将引发许多严重问题.

  • Java多线程volatile原理及用法解析

    首先volatile有两大功能: 保证线程可见性 禁止指令重排序 1.保证线程可见性 首先我们来看这样一个程序,其中不加volatile关键字运行的结果截然不同,加上volatile程序能够正常结束,不加则程序进入死循环: package com.designmodal.design.juc01; import java.util.concurrent.TimeUnit; /** * @author D-L * @Classname T001_volatile * @Version 1.0 *

  • Java并发编程volatile关键字的作用

    日常编程中出现 volatile 关键字的频率并不高,大家可能对 volatile 关键字比较陌生,再深入一点也许是听闻 volatile 只能保证可见性而不能保证原子性,无法有效保证线程安全,于是更加避免使用 volatile ,简简单单加上synchronize关键字就完事了.本文稍微深入探讨 volatile 关键字,分析其作用及对应的使用场景. 并发编程的几个概念简述 首先简单介绍几个与并发编程相关的概念: 可见性 可见性是指变量在线程之间是否可见,JVM 中默认情况下线程之间不具备可见

  • java volatile关键字作用及使用场景详解

    1. volatile关键字的作用:保证了变量的可见性(visibility).被volatile关键字修饰的变量,如果值发生了变更,其他线程立马可见,避免出现脏读的现象.如以下代码片段,isShutDown被置为true后,doWork方法仍有执行.如用volatile修饰isShutDown变量,可避免此问题. public class VolatileTest3 { static class Work { boolean isShutDown = false; void shutdown(

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

    单例模式 回顾一下,单线程下的单例模式代码 饿汉式 构造器私有化 自行创建,并且用静态变量保存static 向外提供这个实例 public 强调这是一个单例,用final public class sington(){ public final static INSTANCE = new singleton(); private singleton(){} } 第二种:jdk1.5之后用枚举类型 枚举类型:表示该类型的对象是有限的几个 我们可以限定为1个,就称了单例 public enum Si

  • Java Volatile关键字实现原理过程解析

    volatile的用法 volatile通常被比喻成"轻量级的synchronized",也是Java并发编程中比较重要的一个关键字.和synchronized不同,volatile是一个变量修饰符,只能用来修饰变量.无法修饰方法及代码块等. volatile的用法比较简单,只需要在声明一个可能被多线程同时访问的变量时,使用volatile修饰就可以了. 如以下代码,是一个比较典型的使用双重锁校验的形式实现单例的,其中使用volatile关键字修饰可能被多个线程同时访问到的single

  • 通过Java读取xml文件内容过程解析

    这篇文章主要介绍了通过Java读取xml文件内容过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 需要下载jar包dom4j:https://dom4j.github.io/ package com.zyb.xml; import java.io.File; import java.util.Iterator; import org.dom4j.Attribute; import org.dom4j.Document; import or

  • java接口私有方法实现过程解析

    这篇文章主要介绍了java接口私有方法实现过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 问题描述: 我们需要抽取一个共有方法,用来解决两个默认方法之间重复代码的问题 但是这个共有方法不应该让实现类使用,应该是私有化的. 解决方案: 从java 9开始,接口当中允许定义私有方法. 1.普通私有方法,解决多个默认方法之间重复代码问题 格式: private 返回值类型方法名称(参数列表){ 方法体 } 2.静态私有方法,解决多个静态方法之

  • Java super关键字调用父类过程解析

    这篇文章主要介绍了Java super关键字调用父类过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 话不多说,直接上代码: package com.my.pac14; /** * @auther Summerday */ public class SuperTest { public static void main(String[] args) { SubClass sb = new SubClass(20); //创建子类的对象,调

  • Java获取配置文件的值过程解析

    这篇文章主要介绍了java获取配置文件的值过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 java大型项目中都会很多系统常量,比如说数据库的账号和密码,以及各种token值等,都需要统一的管理,如果零落的散布到各个类等具体的代码中的话,在后期管理上将是一场灾难,所有需要对这些变量进行统一的管理,一般都会放到web-service.properties文件中,该文件在项目中的位置如下: web-service.properties文件里的

  • Java使用split截取字符串过程解析

    这篇文章主要介绍了Java使用split截取字符串过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 作用背景:一串字符串中的信息有些是有用的有些是多余的,我们需要把多余的信息去掉 例:"11,22,33,44,55" 这串字符串中我们要取出所有非","的内容 public class test { public static void main(String[] args) { String[] all =

  • Java自定义实现equals()方法过程解析

    这篇文章主要介绍了Java自定义实现equals()方法过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 以常见的自定义Date类型为例,没有经验的朋友可能会觉得直接比较年月日即可,从而写出以下的实现 public class MyDate implements Comparable<MyDate> { private final int year; private final int month; private final int

  • Java基于final修饰数据过程解析

    这篇文章主要介绍了Java基于final修饰数据过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 final是Java中的一个重要关键字,它可以修饰数据.方法和类,本篇将从final修饰的数据角度对final做出总结. final修饰的数据代表着:永远不变.意思是,一旦你用final修饰一块数据,你之后就只能看看它,你想修改它,没门. 我们不希望改变的数据有下面两种情况: 永不改变的编译时常量. //编译时知道其值 private fin

  • Java输出Hello World完美过程解析

    1. 你会不会输出"Hello World!"? 图1 图 2 当我们学习一门编程语言的时候,我们都会先学如何输出Hello World!

随机推荐