单例模式能够保证类只有一个实例对象。在Java
中可以使用多种方式来实现单例模式。
Eager initialization
1 | public class SingletonEagerInit { |
该方式在类加载的时候就对实例进行初始化。然后每次调用getInstance
都会返回这个实例对象。
该方式是有缺点,如果这个类的对象非常大,在加载该类后却没有调用getInstance
方法,即加载了类,但没有使用该类的对象。但是这个对象已经被实例化,会占用一部分内存,如果我们用不着这个对象,这些内存就浪费了。
Lazy initialization
1 | public class SingletonLazyInit { |
延迟初始化的方式能够在真正用到实例对象的时候才对其进行初始化。
对整个方法进行synchronized
,避免了多线程下的同步问题,但是对整个方法进行synchronized
效率会下降。
double checked locking
1 | public class SingletonDoubleChecked { |
该方法并为整个方法进行synchronized
,尽可能的缩小了同步块的范围。
字段instance
用volatile
修饰是为了可见性,一致性,顺序性,可见性就是指当对一个临界资源进行修改后,其他线程就能马上觉察到该修改,所有线程看到这个变量的值是一致的,而且编译器也不会对其进行指令重排。
getInstance
方法中使用局部变量result
的原因可以见Effective Java
第二版的第71条。
这个变量的作用是确保实例变量只在已经被初始化的情况下读取一次。虽然这不是严格要求,但是可以提升性能。
我的理解是instance
这个实例变量是在堆内存中分配的,而局部变量result
是在栈中分配的,栈的读取速度要快于堆的读取速度。
如果该单例的类需要实现序列化,那么在加上implements Serializable
后,还需要添加readResolve
方法。该类的实例域都要声明为transient
。
1 | private Object readResolve() { |
对与clone
方法,可以直接抛出异常。
1 | @Override |
Initialization-on-demand holder idiom
1 | public class SingletonByHolder { |
这种方式也是只有在第一次调用getInstance
方法时才进行初始化。在加载SingletonByHolder
类时并不会加载SingletonHolder
类,所以就不会对instance
进行初始化,只有在getInstance
方法中调用SingletonHolder.instance
才会加载SingletonHolder
类,然后进行初始化。
The enum way
1 | public enum Singleton { |
使用枚举来实现单例模式是被推荐的。
这种方法在功能上与公有域方法相近,但是它更加简洁,无偿地提供了序列化机制,绝对防止多次实例化,即使是在面对复杂的序列化或者反射攻击的时候。虽然这种方法还没有广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。