单例模式是最简单也是应用最广泛的设计模式之一,其核心思想是某个类在应用程序的生命周期内只有唯一的实例。这可能是考虑到这个类的职责是很耗费资源的,不适合创建多个对象。或者这是个工具类,在程序的其它地方都可能用到,并且只需要一个对象来保证这个工具具有统一的入口。
例如,在Android应用中,我们经常要在应用启动时对一些SDK或模块进行初始化,或者在其它类中为了避免直接使用Activity等Context会造成内存泄漏,都可能要用到ApplicationContext对象。虽然可以通过Context.getApplicationContext()方法来获取,但是在没有Context时就不方便了。因此,我们通常都会写一个Application的子类,并在其中持有Application的单例:
public class MyApplication extends Application { private static Context applicationContext; @Override public void onCreate() { super.onCreate(); applicationContext = this; //TODO init something } public static Context getApplicationContext() { return applicationContext; } }
这样,就可以在任何地方调用MyApplication.getApplicationContext()方法来获取这个实例了。
而对于一个普通的类,单例模式是有多种实现方式的:
1.饿汉模式
public class HungryInstance { private static HungryInstance instance = new HungryInstance(); private HungryInstance() { } public static HungryInstance getInstance() { return instance; } }
这种实现方式,静态实例在声明时就会进行初始化,即实例的创建在类第一次访问时的静态初始化阶段就会进行。这里要记得将默认构造方法声明为private,保证只能通过getInstance()方法来获取实例。由于在调用静态方法时,实例已经创建好了,因此这里不用考虑线程同步的问题。
2.懒汉模式
public class LazyInstance { private static LazyInstance instance; private LazyInstance() { } public static synchronized LazyInstance getInstance() { if (instance == null) { instance = new LazyInstance(); } return instance; } }
实例在类加载时不会初始化,只有第一次调用getInstance()方法时才会初始化。这在一定程度上避免了没有使用就初始化而造成的资源浪费,适用于需要懒加载的情况。可以看到,getInstance()方法加了synchronized同步锁,保证了多线程访问时实例的唯一性。但是同步锁在第一次调用后就没有必要了,以后每次调用都会同步会造成不必要的开销,这是这种实现方式的缺点。
3.DCL模式(Double Check Lock)
public class DLCInstance {
private volatile static DLCInstance instance;
private DLCInstance() {
}
public static DLCInstance getInstance() {
if (instance == null) {
synchronized (DLCInstance.class) {
if (instance == null) {
instance = new DLCInstance();
}
}
}
return instance;
}
}
DLC模式具有懒汉模式懒加载和线程安全的特点,并且同步锁只会在第一次调用时有效,实例创建后就不会再同步了,从而解决了懒汉模式的缺点。这里实例的声明使用了volatile关键字,即具有原子性。如果不加volatile关键字,instance = new DLCInstance()就是一个非原子的操作,就会存在DLC失效问题。因为在JDK1.5之前,Java内存模型中Cache、寄存器到主存的回写顺序规定以及Java编译器允许处理器乱序执行等原因,可能会出现构造方法执行前,instance指针已经指向了实例所占用的内存空间,即instance不为null。此时如果另一个线程执行到了外层的判空,就会直接返回这个空的instance,从而造成空指针。增加volatile关键字后,因为原子操作是不能被线程调度机制打断的,volatile域会立即被写入到主存中,读取也会发生在主存中,就可以保证instance对象每次都会从主存中读取,从而避免了另一个线程拿到空的instance的情况。(参考《Java编程思想》《Android源码设计模式》)
4.静态内部类模式
public class StaticInnerInstance { private StaticInnerInstance() { } public static StaticInnerInstance getInstance() { return InstanceHolder.instance; } private static class InstanceHolder { private static final StaticInnerInstance instance = new StaticInnerInstance(); } }
这种实现方式同样具有DLC模式的优点。因为instance是在静态内部类中声明和初始化的,是线程安全的。而在调用getInstance()方法前,实例也不会初始化,从而又是懒加载的。并且代码很简洁,不需要判空,是推荐的实现方式。
但是,以上的方式都存在一个问题,都没有处理对象序列化再反序列化后对象的唯一性。因为Serializable序列化对象不会调用构造方法,而是以它存储的二进制位为基础来构造的。当我们反序列化一个单例时,序列化机制会创建一个新的实例。为了解决这个问题,必须在类中加入readResolve()方法:
public class HungryInstance { private static HungryInstance instance = new HungryInstance(); private HungryInstance() { } public static HungryInstance getInstance() { return instance; } private Object readResolve() throws ObjectStreamException { return instance; } }
当对象被序列化后,就会调用这个方法,它返回的对象将成为反序列化时readObject的返回值。因此,这里我们直接返回我们定义的单例,这样就可以解决反序列化单例不唯一的问题。
5.枚举模式
public enum EnumInstance { INSTANCE; public void doSomething() { System.out.println("do something"); } }
使用枚举实现将更加简洁。Java中的enum基本上可以看作是一个常规的类,可以定义字段和方法,只是构造方法有少许限制。enum的构造方法只能在内部用来创建实例,enum定义结束后,Java编译器就不允许使用它来创建任何实例了。因此上面我们声明的这个INSTANCE实例在任何情况下都是一个单例,并且实例的创建也是线程安全的。而enum在反序列化时也不会重新创建对象,也就不用添加readResolve()方法。
在Android开发中,谷歌早已开始宣传Kotlin First的口号。用Kotlin实现单例模式将更加简洁:
object SingleInstance { fun doSomething() { //TODO something } }
Kotlin自带object关键字表示单例类,Kotlin也弱化了静态的概念,我们直接在单例类中定义方法或字段,即可像调用静态方法一样直接调用单例方法SingleInstance.doSomething()。之所以这么简洁,是因为Kotlin将单例的逻辑隐藏了,我们可以通过Android Studio的Show Kotlin Bytecode功能将上述Kotlin代码转换为等价的Java代码:
public final class SingleInstance { public static final SingleInstance INSTANCE; public final void doSomething() { } private SingleInstance() { } static { SingleInstance var0 = new SingleInstance(); INSTANCE = var0; } }
可以看到,实例的初始化放在了静态块里。静态块只会在类首次访问静态成员或创建对象时执行一次,因此Kotlin的单例类可以看作是Java单例的饿汉模式实现。
虽然单例模式的实现方式有多种,但我们只要理解了其核心思想,并根据类的职责,例如是否需要懒加载,是否需要线程安全,是否会进行序列化和反序列化等来选择合适的方式实现即可。
原文:https://www.cnblogs.com/wanghan5950/p/13252628.html