设计模式(2):单例模式SINGLETON

单例模式定义

单例模式(Singleton pattern)限制了类的实例化,并且确保了该类有且仅有一个实例存在。单例类必须提供一个获取实例的全局入口点(global access point)。单例模式类图如下
ShowImage

使用场景

单例模式在应用程序中的使用还是很广的,比如应用的日志管理应用、数据库连接池、线程池等等,这些都应该只有一个实例去操作。

单例模式的实现由以下几个要点:

  • 私有的构造器保证其他类不能对单例类进行实例化
  • 私有的静态变量确保该类只有一个实例
  • 返回该类实例的公有的静态方法,提供给其他类获取单例的接口

单例模式的实现(Implement)

下面介绍多种单例模式的实现

饿汉式单例(Eager Initialization)

这种单例模式实现是最简单的,在饿汉式单例中,单例类的实例在类加载的时候就已经实例化了,虽然这种方法是线程安全的,但是有一些场景饿汉式单例就无法使用了。比如我们的Singleton实例的创建是依赖参数或者配置文件,在getInstance()之前必须动态调用某个方法,这样饿汉式单例就无法使用了。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class EagerInitializationSingleton {
//只有一个实例
private static final EagerInitializationSingleton instance = new EagerInitializationSingleton();
//私有的构造器,阻止其他类实例化
private EagerInitializationSingleton() {
}
//提供获取单例的接口
public static EagerInitializationSingleton getInstance() {
return instance;
}
}

总结

  • 实现简单
  • 无法延迟创建对象

懒汉式单例,线程不安全(LazyInitialization, Thread Not Safe)

现在我们把实例的初始化放到getInstance()方法里面去处理,这样就是懒汉式单例。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class LazyInitializationSingletonA {
//延迟初始化
private static LazyInitializationSingletonA instance;
//私有构造方法
private LazyInitializationSingletonA() {
}
//获取单例,如果没有创建,就在这里创建
public static LazyInitializationSingletonA getInstance() {
if (instance == null) {
instance = new LazyInitializationSingletonA();
}
return instance;
}
}

这段代码实现起来也很简单,并且也使用了懒加载的模式,但是有一个比较严重的问题,如果有多个线程同时调用getInstance(),而此时恰好又没有instance,那么就会创建出多个实例,这样肯定是不符合我们的需要的,所以在多线程的条件下不能使用这种方法创建单例。

总结

  • 实现简单
  • 懒加载机制,可以延迟创建对象
  • 多线程下不能工作

懒汉式单例,线程安全(Lazy Initialization, Thread Safe)

为了让我们的懒汉式单例在多线程的环境下使用,只需要把getInstance()方法设置为同步(synchronized)的即可。虽然这样解决了我们的问题,效率却非常的低,我们想要的是在第一次没有instance的情况对创建实例进行同步的操作,如果按照这种方式,getInstance()方法也只能有一个线程来操作,显然不太符合实际应用场景。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class LazyInitializationSingletonB {
//延迟初始化
private static LazyInitializationSingletonB instance;
//私有构造方法
private LazyInitializationSingletonB() {
}
//获取单例,如果没有创建,就在这里创建
public static synchronized LazyInitializationSingletonB getInstance() {
if (instance == null) {
instance = new LazyInitializationSingletonB();
}
return instance;
}
}

总结

  • 实现也比较简单
  • 懒加载
  • 多线程下能工作,效率低

双重检查锁单例(Double-Checked Locking)

在上面改进过的懒汉式单例中,效率太低的原因是我们把获取instance的方法也变成了同步操作,现在再改进一下,只把创建单例实例的方法设置成同步操作。

代码实现

1
2
3
4
5
6
7
8
9
10
public static DoubleCheckedLockingSingleton getInstance() {
if (instance == null) {
synchronized (DoubleCheckedLockingSingleton.class) {
if (instance == null) {
instance = new DoubleCheckedLockingSingleton();
}
}
}
return instance;
}

现在我们在getInstance()方法里面会有两步操作,第一步判断是否有实例存在,如果没有实例就进入同步代码块,因为有可能会有多个线程同时进入同步代码块操作,所以在同步代码里面再对实例检查一遍是有必要的,否则有可能创建多个实例。改进过后只对创建实例进行同步操作,效率是提高了,但是新的问题又出现了。这行代码instance = new DoubleCheckedLockingSingleton();看起来没什么问题,但是事实上并不是一次原子操作。这条语句实际在JVM中执行的时候干了以下几件事:

  1. 给instance对象分配内存空间
  2. 调用DoubleCheckedLockingSingleton()初始化instance对象
  3. 将instance对象指向已经分配的内存空间

但是JVM的编译器会对指令进行重排序的优化,有可能第3步在第2步之前已经执行过了,如果先执行的第3步,instance此时就不是null类型了,这时其他线程如果执行操作的话,就有可能把还没初始化的instance对象返回使用,这样就出问题了。

那么怎么办呢?只需要在声明instance对象时加上volatile关键字,代码如下。既保证了可见性,同时更重要的是禁止了指令重排序的优化。但是volatile只有在JDK5之后的版本避免重排序才是完全可用的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class DoubleCheckedLockingSingleton {
//使用volatile声明,禁止指令重排序优化,仅在JDK5后面版本使用
private volatile static DoubleCheckedLockingSingleton instance;
private DoubleCheckedLockingSingleton() {
}
public static DoubleCheckedLockingSingleton getInstance() {
if (instance == null) {
synchronized (DoubleCheckedLockingSingleton.class) {
if (instance == null) {
instance = new DoubleCheckedLockingSingleton();
}
}
}
return instance;
}
}

总结

  • 懒加载
  • 性能尚可
  • JDK5之后版本线程安全

静态内部类单例(BillPughSingleton)

由于这种方法是Bill Pugh提出来的,这种单例就以他的名字命名。这种写法实现原理是通过静态内部类来持有单例对象。代码如下

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class BillPughSingleton {
//引入静态内部类保证单例
private static class SingletonHolder {
private static final BillPughSingleton INSTANCE = new BillPughSingleton();
}
//私有构造方法
private BillPughSingleton() {
}
//提供单例访问接口
public static BillPughSingleton getInstance() {
return SingletonHolder.INSTANCE;
}
}

总结

  • 实现不复杂
  • 懒加载方式
  • 不需要同步操作就可以在多线程环境下运行
  • 性能良好
  • 不依赖于JDK版本
  • 推荐使用:)

枚举(Enum)

还有一种特别的方式来写单例,就是使用枚举类型。这种写法十分简单,还可以用来生成多例模式,代码如下

代码实现

Enum单例模式

1
2
3
4
5
6
7
public enum EnumSingleton {
INSTANCE;
public void doSomething() {
System.out.println("enum单例方法...");
}
}

Enum多例模式

1
2
3
public enum EnumMultiton {
instanceA, instanceB, instanceC;
}

总结

  • 实现特别简单
  • 线程安全
  • 防止由于序列化和反射对单例的破坏
  • 不适合在Android中使用,参考Android Training文档(Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.)

单例序列化问题

使用我们的静态内部类进行序列化和反序列化操作之后,发现单例被破坏了。找到一种解决的办法,就是在实现类中提供readResolve()方法的实现。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class SerializeSingletonB implements Serializable{
//实现序列化接口的类,并且提供了readResolve方法的实现
private SerializeSingletonB() {
}
private static class SingletonHelper {
private static final SerializeSingletonB instance = new SerializeSingletonB();
}
public static SerializeSingletonB getInstance() {
return SerializeSingletonB.SingletonHelper.instance;
}
public Object readResolve(){
return getInstance();
}
}

总结

本文分析了多种单例模式的实现方式以及实现原理,主要有懒汉式、饿汉式、双重检查锁、静态内部类、枚举这几种方式。在通常的情况下使用静态内部类的方式,当涉及到序列化和反射的情景时尽可能的使用枚举类型,但是在Android开发时避免使用Enum类型。

使用单例模式的优点

  • 单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁创建销毁时,而且性能又无法优化,单例模式的优势就会非常明显。
  • 单例模式只生成一个实例,所以减少了系统的性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象,则可以通过在应用启动时直接生成一个单例对象,然后用永久驻留内存的方式来解决。
  • 单例模式可以避免对资源的多重占用。
  • 单例模式可以在系统设置全局的访问点,优化和共享资源的访问。

使用单例模式的缺点

  • 单例模式一般没有接口或者抽象,扩展非常困难,一般只能通过直接修改代码的方法来扩展实现。

参考代码

------ 本文结束 ------

版权声明


BillyYccc's blog by Billy Yuan is licensed under a Creative Commons BY-NC-SA 4.0 International License.
本文原创于BillyYccc's Blog,转载请注明原作者及出处!