深入分析Java单例模式的各种方案

简介: 单例模式Java内存模型的抽象示意图:所有单例模式都有一个共性,那就是这个类没有自己的状态。也就是说无论这个类有多少个实例,都是一样的;然后除此者外更重要的是,这个类如果有两个或两个以上的实例的话程序会产生错误。

单例模式

Java内存模型的抽象示意图:

Java内存模型的抽象示意图

所有单例模式都有一个共性,那就是这个类没有自己的状态。也就是说无论这个类有多少个实例,都是一样的;然后除此者外更重要的是,这个类如果有两个或两个以上的实例的话程序会产生错误。

非线程安全的模式

public class Singleton {
  private static Singleton instance;
  private Singleton(){
  }
  public static Singleton getInstance() {
    if (instance == null) //1:A线程执行
      instance = new Singleton(); //2:B线程执行
    return instance;
  }
}

普通加锁

public class SafeLazyInitialization {
    private static Singleton instance;

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

出于性能考虑,采用双重检查加锁的模式

双重检查加锁模式

public class Singleton{
  private static Singleton singleton;
  private Singleton(){

  }

  public static Singleton getInstance(){
    if(null == singleton){  //第一次检查
      synchronized(Singleton.class){  //加锁
        if(null == singleton){  //第二次检查
          singleton = new Singleton();//问题的根源出在这里
        }
      }
    }
    return singleton;
  }
}

双重检查加锁模式相对于普通的单例和加锁模式而言,从性能和线程安全上来说都有很大的提升和保障。然而双重检查加锁模式也存在一些隐蔽不易被发现的问题。首先我们要明白在JVM创建新的对象时,主要要经过三个步骤。

  • 分配内存
  • 初始化构造器
  • 将对象指向分配的内存地址

这样的顺序在双重加锁模式下是么有问题的,对象在初始化完成之后再把内存地址指向对象。

问题的根源

但是现代的JVM为了追求执行效率会针对字节码(编译器级别)以及指令和内存系统重排序(处理器重排序)进行调优,这样的话就有可能(注意是有可能)导致2和3的顺序是相反的,一旦出现这样的情况问题就来了。

java源代码到最终实际执行的指令序列:
java源代码到最终实际执行的指令序列

前面的双重检查锁定示例代码的(instance = new Singleton();)创建一个对象。这一行代码可以分解为如下的三行伪代码:

memory = allocate();   //1:分配对象的内存空间
ctorInstance(memory);  //2:初始化对象
instance = memory;     //3:设置instance指向刚分配的内存地址

上面三行伪代码中的2和3之间,可能会被重排序(在一些JIT编译器上,这种重排序是真实发生的,详情见参考文献1的“Out-of-order writes”部分)。2和3之间重排序之后的执行时序如下:

memory = allocate();   //1:分配对象的内存空间
instance = memory;     //3:设置instance指向刚分配的内存地址
                       //注意,此时对象还没有被初始化!
ctorInstance(memory);  //2:初始化对象

1008100.png

多线程并发执行的时候的情况:

1008101.png

解决方案

基于Volatile的解决方案

先来说说Volatile这个关键字的含义:

  • 可以很好地解决可见性问题
  • 但不能确保原子性问题(通过 synchronized 进行解决)
  • 禁止指令的重排序(单例主要用到此JVM规范)

Volatile 双重检查加锁模式

public class Singleton{
  private volatile static Singleton singleton;
  private Singleton(){
  }

  public static Singleton getInstance(){
    if(null == singleton){
      synchronized(Singleton.class){
        if(null == singleton){
          singleton = new Singleton();
        }
      }
    }
    return singleton;
  }
}

基于类初始化的解决方案

利用静态内部类的方式来创建,因为静态属性由JVM确保第一次初始化时创建,因此也不用担心并发的问题出现。当初始化进行到一半的时候,别的线程是无法使用的,因为JVM会帮我们强行同步这个过程。另外由于静态变量只初始化一次,所以singleton仍然是单例的。

img_9ae16c5b73ae504651215234ef436cb1.png

这个方案的实质是:允许“问题的根源”的三行伪代码中的2和3重排序,但不允许非构造线程(这里指线程B)“看到”这个重排序。

静态内部类的方式

public class Singleton{

  private Singleton(){}

  public static Singleton getInstance(){
    return InnerClassSingleton.singleton;
  }

  private class InnerClassSingleton{
    protected static Singleton singleton = new Singleton();
  }
}

然而,虽然静态内部类模式可以很好地避免并发创建出多个实例的问题,但这种方式仍然有其存在的隐患。

存在的隐患

  • 一旦一个实例被持久化后重新生成的实例仍然有可能是不唯一的。
  • 由于java提供了反射机制,通过反射机制仍然有可能生成多个实例。

序列化和反序列化带来的问题:反序列化后两个实例不一致了。

private static void singleSerializable() {
    try (FileOutputStream fileOutputStream=new FileOutputStream(new File("myObjectFilee.txt"));
         ObjectOutputStream objectOutputStream=new ObjectOutputStream(fileOutputStream);) {
//            SingletonObject singletonObject = SingletonObject.getInstance();
//            InnerClassSingleton singletonObject = InnerClassSingleton.getInstance();
        EnumSingleton singletonObject = EnumSingleton.INSTANCE;
        objectOutputStream.writeObject(singletonObject);
        objectOutputStream.close();
        fileOutputStream.close();
        System.out.println(singletonObject.hashCode());
    } catch (IOException e) {
        e.printStackTrace();
    }

    try (FileInputStream fileInputStream=new FileInputStream(new File("myObjectFilee.txt"));
         ObjectInputStream objectInputStream=new ObjectInputStream(fileInputStream);) {

//            SingletonObject singleTest=(SingletonObject) objectInputStream.readObject();
//            InnerClassSingleton singleTest=(InnerClassSingleton) objectInputStream.readObject();
        EnumSingleton singleTest=(EnumSingleton) objectInputStream.readObject();
        objectInputStream.close();
        fileInputStream.close();
        System.out.println(singleTest.hashCode());
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
}

问题点及解决办法
ObjectInputStream中的readOrdinaryObject

if (obj != null &&
    handles.lookupException(passHandle) == null &&
    desc.hasReadResolveMethod())
{
    Object rep = desc.invokeReadResolve(obj);
    if (unshared && rep.getClass().isArray()) {
        rep = cloneArray(rep);
    }
    if (rep != obj) {
        handles.setObject(passHandle, obj = rep);
    }
}

调用自定义的readResolve方法

protected Object readResolve(){
    System.out.println("调用了readResolve方法!");
    return  InnerClassSingleton.getInstance();
}

通过反射机制获取到两个不同的实例

private static void attack() {
    try {
        Class<?> classType = InnerClassSingleton.class;
        Constructor<?> constructor = classType.getDeclaredConstructor(null);
        constructor.setAccessible(true);
        InnerClassSingleton singleton = (InnerClassSingleton) constructor.newInstance();
        InnerClassSingleton singleton2 = InnerClassSingleton.getInstance();
        System.out.println(singleton == singleton2);  //false
    } catch (NoSuchMethodException e) {
        e.printStackTrace();
    } catch (InstantiationException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    } catch (InvocationTargetException e) {
        e.printStackTrace();
    }
}

解决方案 : 私有构造方法中进行添加标志判断。

private InnerClassSingleton() {
    synchronized (InnerClassSingleton.class) {
        if (false == flag) {
            flag = !flag;
        } else {
            throw new RuntimeException("单例模式正在被攻击");
        }
    }
}

单例最优方案,枚举的方式

枚举实现单例的优势

  • 自由序列化;
  • 保证只有一个实例(即使使用反射机制也无法多次实例化一个枚举量);
  • 线程安全;
public enum Singleton {
    INSTANCE;

    private Singleton(){}
}

Hibernate的解决方案

通过ThreadLocal的方式

import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.cfg.Configuration;
public class HibernateSessionFactory {
    private static String CONFIG_FILE_LOCATION = "/hibernate.cfg.xml";
    private static final ThreadLocal threadLocal = new ThreadLocal();
    private static Configuration configuration = new Configuration();
    private static org.hibernate.SessionFactory sessionFactory;
    private static String configFile = CONFIG_FILE_LOCATION;

    static {
       try {
           configuration.configure(configFile);
           sessionFactory = configuration.buildSessionFactory();
       } catch (Exception e) {
           System.err.println("%%%% Error Creating SessionFactory %%%%");
           e.printStackTrace();
       }
    }

    private HibernateSessionFactory() {
    }

    public static Session getSession() throws HibernateException {
       Session session = (Session) threadLocal.get();
       if (session == null || !session.isOpen()) {
           if (sessionFactory == null) {
              rebuildSessionFactory();
           }
           session = (sessionFactory != null) ? essionFactory.openSession() : null;
           threadLocal.set(session);
       }
       return session;
    }
// Other methods...
}

参考文档:

相关文章
|
3月前
|
缓存 自然语言处理 前端开发
深入分析Java编译原理
深入分析Java编译原理
47 0
|
3月前
|
算法 Java 程序员
论文翻译 | 【深入挖掘Java技术】「底层原理专题」深入分析一下并发编程之父Doug Lea的纽约州立大学的ForkJoin框架的本质和原理
本文深入探讨了一个Java框架的设计、实现及其性能。该框架遵循并行编程的理念,通过递归方式将问题分解为多个子任务,并利用工作窃取技术进行并行处理。所有子任务完成后,其结果被整合以形成完整的并行程序。 在总体设计上,该框架借鉴了Cilk工作窃取框架的核心理念。其核心技术主要聚焦于高效的任务队列构建和管理,以及工作线程的管理。经过实际性能测试,我们发现大多数程序的并行加速效果显著,但仍有优化空间,未来可能需要进一步研究改进方案。
48 3
论文翻译 | 【深入挖掘Java技术】「底层原理专题」深入分析一下并发编程之父Doug Lea的纽约州立大学的ForkJoin框架的本质和原理
|
Java C++
【Java技术专题】「原理专题」深入分析Java中finalize方法的作用和底层原理
【Java技术专题】「原理专题」深入分析Java中finalize方法的作用和底层原理
136 0
【Java技术专题】「原理专题」深入分析Java中finalize方法的作用和底层原理
|
存储 缓存 Java
【Java深层系列】「并发编程系列」深入分析和研究MappedByteBuffer的实现原理和开发指南
【Java深层系列】「并发编程系列」深入分析和研究MappedByteBuffer的实现原理和开发指南
149 1
|
安全 算法 Oracle
【Java技术指南】「编译器专题」深入分析探究“静态编译器”(JAVA\IDEA\ECJ编译器)是否可以实现代码优化?
【Java技术指南】「编译器专题」深入分析探究“静态编译器”(JAVA\IDEA\ECJ编译器)是否可以实现代码优化?
215 0
|
缓存 Java
【Java原理探索】深入分析JDK动态代理的源码 | Java开发实战
【Java原理探索】深入分析JDK动态代理的源码 | Java开发实战
58 0
|
缓存 Java
深入分析JDK动态代理实现原理|Java 开发实战
深入分析JDK动态代理实现原理|Java 开发实战
81 0
|
Java 调度
【Java原理探索】深入分析Mutex锁的运行原理
【Java原理探索】深入分析Mutex锁的运行原理
177 0
|
设计模式 存储 Java
《深入分析Java Web技术内幕》深入之后的理解
## 0.前言 近期阅读了《深入分析Java Web技术内幕》这本书,书中涉及的知识点非常全面,让我对Java Web相关技术有了更加全面的认识,受益匪浅。这里跟大家分享下本人深入后印象比较深刻的几个点。 ## 1.NIO (Non-blocking IO) ### 1.1同步异步、阻塞非阻塞 - I/O交互中经常涉及同步/异步,阻塞/非阻塞。 - 要注意这两个是不同纬度
《深入分析Java Web技术内幕》深入之后的理解
|
缓存 Java 编译器
深入分析java内存模型(注意和java内存结构的区别)
最近在更java多线程相关的文章,正好有人问我一些java内存模型的问题,因此花了一些时间,好好地了解一下。本篇文章主要是为了解决以下几个问题? 1、java内存模型和java内存结构有什么区别? 2、为什么要有内存模型? 3、java的内存模型是什么样子的? 这篇文章,基本上不会涉及到代码,全是一些概念性的知识,但是也是面试常问和java进阶所需要掌握的必要的基本知识点,所以,希望你耐着性子,慢慢来。
382 3
深入分析java内存模型(注意和java内存结构的区别)