单例模式

Posted by Pismery Liu on Sunday, December 16, 2018

TOC

单例模式

意图

解决一个类不需要多次实例化,系统只需要一个实例化对象;或者资源占用太大只允许创建一个。

优缺点

单例模式:
优点:节约系统资源;
缺点:

  1. 没有抽象层不易扩展;
  2. 将创建对象职责和使用对象职责都安排给当单例类;
  3. 对于有垃圾回收机制的语言如Java,C#。长时间不被利用容易导致被回收重新实例化;导致共享状态丢失;

适用场景

  1. 系统只需要一个实例,或者实例占用资源太多只允许一个实例;

饿汉式单例

public class EagerSingleton {

    private static final EagerSingleton singleton = new EagerSingleton();

    private EagerSingleton() {
    }

    public static EagerSingleton getInstance() {
        return singleton;
    }
}
public class EagerSingleton {

    private static final EagerSingleton singleton;

    static {
        singleton =  = new EagerSingleton();
        //.... 更多初始化操作
    }

    private EagerSingleton() {
    }

    public static EagerSingleton getInstance() {
        return singleton;
    }
}

懒汉式单例

双重检测锁方式(Double-Checked Locking DCL)

public class LazySingleton {
    private volatile static LazySingleton singleton; //留意volatile关键字

    private LazySingleton() {
    }

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

注意singleton成员属性需要加上关键字volatile,来防止指令重排序。因为一个new操作在可以分解为以下三个操作:

  1. 为singleton申请内存空间
  2. 初始化singleton对象
  3. 将singleton指向分配的内存地址

而Java虚拟机可能会发生指令重排序导致线程以1->3->2的顺序执行。这就可能导致线程T1执行了1->3后中断,导致线程T2以为创建完毕,而实际未创建完毕。

内部类单例

利用静态内部类延迟加载,即内部类Holder直到第一次调用getInstance方法才加载。 这种解决方式是天然的线程安全的,由Java虚拟机classloader机制保证其线程安全。

public class IoDHSingleton {

    private IoDHSingleton() {
    }

    private static class Holder {
        private static final IoDHSingleton singleton = new IoDHSingleton();
    }

    public static IoDHSingleton getInstance() {
        return Holder.singleton;
    }

}

枚举类单例

public enum EnumSingleton {
    ENUM_SINGLETON;

    EnumSingleton() {
    }

    public EnumSingleton getSingleton() {
        return this;
    }
}
## 客户端
@Slf4j
public class Client {

    public static void main(String[] args) {
        eagerSingleton();
        lazySingleton();
        ioDHSingleton();
        enumSingleton();
    }

    private static void eagerSingleton() {
        EagerSingleton e1 = EagerSingleton.getInstance();
        EagerSingleton e2 = EagerSingleton.getInstance();
        log.debug("EagerSingleton e1 == e2: " + String.valueOf(e1 == e2));
    }

    private static void lazySingleton() {
        LazySingleton e1 = LazySingleton.getInstance();
        LazySingleton e2 = LazySingleton.getInstance();
        log.debug("LazySingleton e1 == e2: " + String.valueOf(e1 == e2));
    }

    private static void ioDHSingleton() {
        IoDHSingleton e1 = IoDHSingleton.getInstance();
        IoDHSingleton e2 = IoDHSingleton.getInstance();
        log.debug("IoDHSingletonF e1 == e2: " + String.valueOf(e1 == e2));
    }

    private static void enumSingleton() {
        EnumSingleton e1 = EnumSingleton.ENUM_SINGLETON;
        EnumSingleton e2 = EnumSingleton.ENUM_SINGLETON;
        EnumSingleton e3 = EnumSingleton.valueOf("ENUM_SINGLETON");

        log.debug("EnumSingleton e1 == e2: " + String.valueOf(e1 == e2));
        log.debug("EnumSingleton e1 == e3: " + String.valueOf(e1 == e3));
    }

}

运行结果

单例与反射

Java中的反射Api是一个功能十分强大的Api。尽管构造函数私有,通过反射,我们都能够调用对象的构造函数初始化对象。这违背了单例的初衷。

代码演示

import java.lang.reflect.Constructor;

class MySingleton {
    private static final MySingleton instance = new MySingleton();

    private MySingleton() {}

    public static MySingleton getInstance() {
        return instance;
    }
}

public class SingletonAndReflection {
    public static void main(String[] args) {
        MySingleton singletonInstance = MySingleton.getInstance();
        MySingleton reflectionInstance = null;
        
        try {
            Constructor[] constructors = MySingleton.class.getDeclaredConstructors();
            for (Constructor constructor : constructors) {
                constructor.setAccessible(true);
                reflectionInstance = (MySingleton) constructor.newInstance();
            }
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }

        System.out.println("singletonInstance hashCode: " + singletonInstance.hashCode());
        System.out.println("reflectionInstance hashCode: " + reflectionInstance.hashCode());
    }
}

运行结果

# Output
singletonInstance hashCode: 1618212626
reflectionInstance hashCode: 947679291

解决方式

在构造函数中判断对象是否已经初始化。通过枚举类实现的单例,天然能够防止方式,

class MySingleton {
    private static final MySingleton instance = new MySingleton();

    private MySingleton() {
        // protect against instantiation via reflection
        if(instance != null) {
            throw new IllegalStateException("Singleton already initialized");
        }
    }

    public static MySingleton getInstance() {
        return instance;
    }
}

单例与序列化

在编码过程中,我们经常需要对class进行序列化和反序列化,而当单例对象经过序列化后再反序列化将生成新的对象,这违背了单例模式的初衷。

代码演示

import java.io.*;

class SerializableSingleton implements Serializable {
    private static final long serialVersionUID = 8806820726158932906L;

    private static SerializableSingleton instance;

    private SerializableSingleton() {}

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

public class SingletonAndSerialization throws Exception{
    public static void main(String[] args) {
        SerializableSingleton instance1 = SerializableSingleton.getInstance();
        
        // Serialize singleton object to a file.
        ObjectOutput out = new ObjectOutputStream(new FileOutputStream("singleton.ser"));
        out.writeObject(instance1);
        out.close();

        // Deserialize singleton object from the file
        ObjectInput in = new ObjectInputStream(new FileInputStream("singleton.ser"));
        SerializableSingleton instance2 = (SerializableSingleton) in.readObject();
        in.close();

        System.out.println("instance1 hashCode: " + instance1.hashCode());
        System.out.println("instance2 hashCode: " + instance2.hashCode());
    }
}

运行结果

# Output
instance1 hashCode: 1348949648
instance2 hashCode: 434091818

解决方式

在单例类中实现readResolve()方法,返回已存在的实例。readResolve()方法会在反序列化时调用。同样枚举类实现的单例天然避免了序列化的问题。

class SerializableSingleton implements Serializable {
    private static final long serialVersionUID = 8806820726158932906L;

    private static SerializableSingleton instance;

    private SerializableSingleton() {}

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

    // implement readResolve method to return the existing instance
    protected Object readResolve() {
        return instance;
    }
}

总结

注意事项

  1. 懒汉式单例注意静态成员变量要使用volatile关键字
  2. 饿汉式单例使用静态成员常量。若初始化比较复杂 可以在static块中初始化
  3. 内部类单例注意内部类是静态内部类

优缺点

懒汉式单例:
优点:能够延迟加载单例类;
缺点:但是因为需要确保多线程下的安全需要使用双重检测锁定机制(DCL:Double-Check Locking),对性能有所降低;无法阻止反射

饿汉式单例:
优点:不需要考虑锁机制,实现简单效率高;
缺点:不能够延迟加载单例类;无法阻止反射

内部类单例:
优点:能够延迟加载单例类,实现简单,不需要考虑线程安全,交由Java虚拟机classloader机制保证其线程安全;
缺点:缺点对语言方面有限制很多面向对象语言不支持;无法阻止反射

枚举类单例:
优点:能够阻止反射、序列化、并且线程安全
缺点:不能够延迟加载单例类