当前位置: 首页 > news >正文

深入分析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仍然是单例的。

1008103.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...
}

参考文档:

  • http://www.ibm.com/developerworks/java/library/j-dcl/index.html
  • 《Java 并发变成的艺术》
  • 《Effective Java中文版第2版》
  • http://www.cnblogs.com/345214483-qq/p/6472158.html
  • http://www.cnblogs.com/lthIU/p/6240128.html
  • http://www.uml.org.cn/sjms/201110184.asp

相关文章:

  • 左手书法二十七篇
  • Flink - NetworkEnvironment
  • 修改Jmeter配置使能支持更大并发
  • 关于grep正则表达式-1
  • web前端性能优化总结
  • Cloudera Manager是啥?主要是干啥的?
  • Android中的动画,选择器,样式和主题的使用
  • 基于zedboard的DMA设计笔记
  • WePY 在小程序性能调优上做出的探究
  • EL表达式学习笔记
  • Lucene 高级搜索
  • linux 小白启航之路-搭建linuxDHCP中继服务器
  • hdu 4122 Alice#39;s mooncake shop (线段树)
  • 三栏布局总结
  • date命令使用文档
  • Android框架之Volley
  • angular2开源库收集
  • express如何解决request entity too large问题
  • Facebook AccountKit 接入的坑点
  • React的组件模式
  • SOFAMosn配置模型
  • VirtualBox 安装过程中出现 Running VMs found 错误的解决过程
  • vuex 学习笔记 01
  • windows下如何用phpstorm同步测试服务器
  • 第十八天-企业应用架构模式-基本模式
  • 关于Android中设置闹钟的相对比较完善的解决方案
  • 函数式编程与面向对象编程[4]:Scala的类型关联Type Alias
  • 记一次删除Git记录中的大文件的过程
  • 每个JavaScript开发人员应阅读的书【1】 - JavaScript: The Good Parts
  • 前端相关框架总和
  • 实现简单的正则表达式引擎
  • 小程序开发之路(一)
  • 移动端 h5开发相关内容总结(三)
  • 用简单代码看卷积组块发展
  • No resource identifier found for attribute,RxJava之zip操作符
  • mysql 慢查询分析工具:pt-query-digest 在mac 上的安装使用 ...
  • (a /b)*c的值
  • (Redis使用系列) Springboot 实现Redis消息的订阅与分布 四
  • (zt)最盛行的警世狂言(爆笑)
  • (多级缓存)多级缓存
  • (附源码)springboot码头作业管理系统 毕业设计 341654
  • (附源码)ssm本科教学合格评估管理系统 毕业设计 180916
  • (接口自动化)Python3操作MySQL数据库
  • (区间dp) (经典例题) 石子合并
  • (一)Dubbo快速入门、介绍、使用
  • (转)shell中括号的特殊用法 linux if多条件判断
  • ****Linux下Mysql的安装和配置
  • .gitignore文件_Git:.gitignore
  • .mysql secret在哪_MYSQL基本操作(上)
  • .NET 6 在已知拓扑路径的情况下使用 Dijkstra,A*算法搜索最短路径
  • .NET MVC之AOP
  • .Net 中Partitioner static与dynamic的性能对比
  • .NET/C# 编译期间能确定的相同字符串,在运行期间是相同的实例
  • .NET/C# 使用反射注册事件
  • .NetCore项目nginx发布