设计模式——单例模式

155 阅读4分钟

单例模式

概述

在一些类中,我们可能只需要一个实例,而单例模式就是指一个类中仅有一个实例,并提供一个访问该实例的方法。单例类拥有一个私有构造函数,确保用户无法通过new关键字直接实例化它,该模式中包含一个静态私有成员变量与静态公有的工厂方法,该工厂方法负责检验实例的存在性并实例化自己,然后存储在静态成员变量中,以确保只有一个实例被创建。

优点

由于在系统内存中只存在一个对象,因此对于一些需要频繁创建和销毁的对象,单例模式可以减轻GC压力,缩短GC停顿时间。

在单例模式下,我们可以在需要使用对象时才创建对象,避免资源浪费;而如果是使用全局变量来确保一个类只有一个实例,那么假设这个实例非常消耗资源,且在程序的执行中大多时候都没有使用到,这样会造成资源的浪费。

单例模式的实现

单例模式有两种构建方式:

(1)饿汉模式

该方式下的实例是在类装载时构建,即JVM在加载这个类时就立即创建唯一的实例,无论该实例是否会被使用到。这种方式是线程安全的。

    public class Singleton {
       //在静态初始化器中创建单例实例,保证了线程安全
        private static Singleton uniqueInstance = new Singleton();
        private Singleton(){}
        public static Singleton getInstance(){
            return uniqueInstance;
        }
    }

(2)懒汉式

该方式下的实例是在第一次被使用时构建

public class Singleton {    
      private static Singleton uniqueInstance;  
      private Singleton (){
      }   
      //没有加入synchronized关键字的版本是线程不安全的
      public static Singleton getInstance() {
	      if (uniqueInstance == null) {  //判断当前示例是否存在
	          uniqueInstance = new Singleton();  
	      }  
	      return uniqueInstance;  
      }  
 }

上面的实现方式是非线程安全的,因此我们可以加入synchronized关键字和volatile,在发现实例没有被创建后,再对实例化操作加锁。

public class Singleton {    //双重检查加锁

    //volatile保证了 当实例被初始化后,其他线程能发现到其变化
    private volatile static Singleton uniqueInstance;
    private Singleton() {}
    public static Singleton getInstance() {
        if (uniqueInstance == null) {
            synchronized(Singleton.class) {
                if (uniqueInstance == null) {   //再检查一次
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

此外懒汉式还有静态内部,枚举的实现方式:

public class Singleton {  
/*  静态内部类方式
    只有当调用了getInstance 方法时,才会装载SingletonHolder类,
    从而实例化instance。由于类加载是单线程执行的,
    这种方式通过 JVM的类加载机制确保线程安全。
*/
    private static class SingletonHolder {  
         private static final Singleton INSTANCE = new Singleton();  
    }  
    private Singleton (){}  
    public static final Singleton getInstance() {  
        return SingletonHolder.INSTANCE;  
    }  
}   
/*
    枚举方式
    这种方式更简洁,且若单例类可实例化,则它可以防止 在反序列化中被多次创建实例
*/
enum Singleton {
    INSTANCE;
    public void sayHello() {
        System.out.println("halo");
    }
}

public class Main {
    public static void main(String[] args) {
        Singleton.INSTANCE.sayHello();
    }
}

单例模式与序列化

首先创建一个可以被序列化的单例类:

public class Singleton implements Serializable{
    private volatile static Singleton singleton;
    private Singleton (){}
    public static Singleton getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

然后测试一下通过反序列化得到的单例类实例和已加载的单例类实例是否是一个对象

public class Test {
//此处省略流的关闭
    public static void main(String[] args) throws Exception{
        String filePath = "F:\\Singleton.txt";
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filePath));
        oos.writeObject(Singleton.getSingleton());

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filePath));
        Singleton newInstance = (Singleton)ois.readObject();
        //判断反序列化得到的对象与单例类中已加载的对象是否是一个对象
        System.out.println(newInstance == Singleton.getSingleton());  
        //输出false
    }
}

输出的结果是false,表明通过对Singleton的序列化与反序列化得到的对象是一个新的对象,这就破坏了Singleton的单例性。

为什么会出现这样的问题?

下面是反序列化中读对象的readObject方法中涉及到的一个方法readOrdinaryObject()的一部分源码:

Object obj;
try {
/* 
    isInstantiable方法:若这个序列化的类可以在运行时被实例化,则返回true.
    
    newInstance方法:通过反射的方式调用无参构造方法新建一个对象,尽管这个
    无参构造方法是私有的。
*/
    obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
    throw (IOException) new InvalidClassException(desc.forClass().getName(),"unable to create instance").initCause(ex);
}

可以看出反序列化通过反射来 调用无参数的私有构造方法创建一个新对象,这就是原因。

如何防止序列化破坏单例模式?

在来看一下readOrdinaryObject()方法的另一部分源码:

if (obj != null &&
            handles.lookupException(passHandle) == null &&
                        desc.hasReadResolveMethod()) {
            /*
                hasReadResolveMethod方法:若可序列化的类中包含readResolve
                                        方法,则返回true。
                invokeReadResolve方法:通过反射的方式调用
                                        要被反序列化的类的readResolve方法。
            */
            Object rep = desc.invokeReadResolve(obj);
            if (unshared && rep.getClass().isArray()) {
                rep = cloneArray(rep);
            }
            if (rep != obj) {
                handles.setObject(passHandle, obj = rep);
            }
        }

因此我们只需要在序列化的单例类中实现readResolve方法即可:

public class Singleton implements Serializable{
    private volatile static Singleton singleton;
    private Singleton (){}
    public static Singleton getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
    private Object readResolve() {
        return singleton;
    }
}

参考资料

JavaGuide

单例与序列化的那些事儿