TOC
单例模式
意图
解决一个类不需要多次实例化,系统只需要一个实例化对象;或者资源占用太大只允许创建一个。
优缺点
单例模式:
优点:节约系统资源;
缺点:
- 没有抽象层不易扩展;
- 将创建对象职责和使用对象职责都安排给当单例类;
- 对于有垃圾回收机制的语言如Java,C#。长时间不被利用容易导致被回收重新实例化;导致共享状态丢失;
适用场景
- 系统只需要一个实例,或者实例占用资源太多只允许一个实例;
饿汉式单例
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操作在可以分解为以下三个操作:
- 为singleton申请内存空间
- 初始化singleton对象
- 将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;
}
}
总结
注意事项
- 懒汉式单例注意静态成员变量要使用volatile关键字
- 饿汉式单例使用静态成员常量。若初始化比较复杂 可以在static块中初始化
- 内部类单例注意内部类是静态内部类
优缺点
懒汉式单例:
优点:能够延迟加载单例类;
缺点:但是因为需要确保多线程下的安全需要使用双重检测锁定机制(DCL:Double-Check Locking),对性能有所降低;无法阻止反射
饿汉式单例:
优点:不需要考虑锁机制,实现简单效率高;
缺点:不能够延迟加载单例类;无法阻止反射
内部类单例:
优点:能够延迟加载单例类,实现简单,不需要考虑线程安全,交由Java虚拟机classloader机制保证其线程安全;
缺点:缺点对语言方面有限制很多面向对象语言不支持;无法阻止反射
枚举类单例:
优点:能够阻止反射、序列化、并且线程安全
缺点:不能够延迟加载单例类