之前在面试中,被面试官问到了设计模式,很自信的说了解单例模式。然后问我知道哪些实现方式,说出你觉得最好的是哪一种方式。当时想,以前看到的不就是懒汉和饿汉模式吗?然后说了下以及优缺点,结果被面试官狠狠鄙视了一顿,问你就知道这2个?于是回来后决定好好再看下单例模式,并做一下总结。
单例模式首先要保证线程安全,所以一切线程不安全的单例模式都不在讨论范围内。
1、懒汉模式
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
这种写法确实能保证线程安全,但是效率十分低下,每次都需要用到synchronized关键字同步。但是实际情况下,99%情况是不需要同步的
2、饿汉模式
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}
这种方式基于类加载机制避免了多线程的同步问题,不过,instance在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用getInstance方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance显然没有达到lazy loading的效果,同时如果没有用到这个实例还会导致内存被占用浪费。
3、懒汉模式升级版(双重判空校验)
public class Singleton {
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;
}
}
这种模式看起来已经非常完美,可以解决经常会调用synchronized的问题,也不会在没有真正使用对象的时候浪费内存。之前我对单例模式的了解只到这里。
4、静态内部类
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
这种方法可以说是饿汉模式升级版,同样是利用类加载机制保证安全,避免了synchronized的效率问题(虽然比起上面的方法提升不大,但还是有一定提升的),也保证了lazy loading的效果。
5、枚举
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
effective java上强烈推荐的单例模式,很可惜我之前看这书时候忽略掉了这部分(当时觉得单例应该讲不出什么新东西了吧),它不仅能避免多线程同步问题,最关键还能防止反序列化和反射重新创建新的对象,唯一的缺陷可能就是不能延迟加载了。
这里其实开始看的时候会有一些疑惑,后来仔细查看资料和书籍后发现还是自己的基础知识点掌握的不牢固导致的。
1、为什么枚举能避免多线程同步问题?
2、为什么枚举能防止反序列化重新创建新的对象?
3、为什么枚举能防止反射创建新的对象?
4、反序列化如果不想用枚举的方式去创建新的对象,那需要怎么做?
总结:
1、因为枚举在JVM中实际上是通过继承java.lang.Enum实现的,所以枚举本身就是一个类,通过反编译可以知道,在JVM中会将枚举变成一个抽象类,同时通过静态代码块对对象进行分配,而静态代码块\是由虚拟机保证多线程情况下的同步问题的,所以enum没有同步问题。
2、在java的序列化实现中,实际上是通过ObjectInputStream和ObjectOutputStream来完成的,java在ObjectInputStream对枚举做了单独处理,不会通过readObject来重新获取一个新对象,而是通过Enum的valueOf来获取到对象,而valueOf不会改变对象是由于在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。
3、和第一点原因一样,因为枚举本身是一个抽象类,抽象类是不允许实例化的,所以保证了反射安全。
4、通过加入readResolve的形式,让原有对象不会因为readObject而改变,源码如下。
if (classDesc.hasMethodReadResolve()){
Method methodReadResolve = classDesc.getMethodReadResolve();
try {
result = methodReadResolve.invoke(result, (Object[]) null);
} catch (IllegalAccessException ignored) {
} catch (InvocationTargetException ite) {
Throwable target = ite.getTargetException();
if (target instanceof ObjectStreamException) {
throw (ObjectStreamException) target;
} else if (target instanceof Error) {
throw (Error) target;
} else {
throw (RuntimeException) target;
}
}
}
在ObjectInputStream调用readObject的时候,会对当前类是否有readResolve做判断,如果有,就调用这个方法,通过反射获取到对象并对之前new出来的对象做替换。
一个单例模式,涉及到的知识点包括了线程安全,类加载机制,枚举实现原理,序列化,反射等多个知识点。即使是已经学过,仍然有回顾的价值。