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

  1. 云栖社区>
  2. 博客>
  3. 正文

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

无声胜有声 2017-04-03 18:51:00 浏览332 评论0

摘要: 单例模式 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...
}

参考文档:

【云栖快讯】一站式开发者服务,海量学习资源免费学  详情请点击

网友评论