设计模式系列(一):单例模式

谈到设计模式,相信很多人脑子里蹦出的第一个就是单例模式。好的,如你所愿,今天设计模式的第一篇,我们从单例模式开始谈起。

使用场景

在一个系统中,要求一个类有且一个对象,如果出现多个对象会带来不必要的麻烦,可以采用单例模式。具体的场景有:

  • 要求生成唯一序列号的环境。

  • 在整个项目中需要一个共享访问点或共享数据,如计数器。

  • 创建一个对象需要消耗的资源过多,如要访问IO和数据库等资源。

  • 需要定义大量的静态常亮和静态方法,如工具类。

一 饿汉式

首先是最简单的实现方式,饿汉式。

为了保证类只能创建出一个对象,我们首先需要将构造方法设置为私有,无法为外界所调用。接着在类加载(应用启动)时就初始化一个对象出来。最后提供一个静态公共方法,获取这个对象。

1
2
3
4
5
6
7
8
9
public class Singleton{
private static Singleton instance = new Singleton();

private Singleton(){}

public static Singleton getInstance(){
return instance;
}
}

二 懒汉式

基础版

上面的饿汉式简单粗暴,但有一个缺点,就是在类加载时就需要初始化这个对象。如果单例占用内存很少,初始化速度很快,这样使用没什么问题。但是如果单例占用的内存比较大,而应用对启动速度又有要求,我们就需要对上面的方式加以优化了。

1
2
3
4
5
6
7
8
9
10
11
12
public class Singleton{
private static Singleton instance = null;

private Singleton(){}

public static Singleton getInstance(){
if(null == instance){
instance = new Singleton();
}
return instance;
}
}

懒汉式顾名思义,只有在需要的时候,才进行单例的初始化操作。首次调用会创建出新的对象,再次调用会直接返回之前已创建出来的对象。

同步锁

乍一看上去,上面基础版的写法完美解决了问题。但细看看,这样写在多线程时,可能会带来新的问题。如果现在有两个线程同时在执行getInstance()方法,线程A刚进行了判断,还没来得及创建对象,这时线程B也执行到了判断,会发现instance仍然还是为null。这个时候所谓的单例就名不符实了,因为创建出了两个对象。而饿汉式不会出现这个问题,是因为JVM只会加载一次。

熟悉点并发知识会很容易解决这个问题。既然getInstance()是线程不安全的,那么我们给它加上一个同步锁就好了。

1
2
3
4
5
6
7
8
9
10
11
12
public class Singleton{
private static Singleton instance = null;

private Singleton(){}

public synchronized static Singleton getInstance(){
if(null == instance){
instance = new Singleton();
}
return instance;
}
}

双重校验锁

上面的同步锁写法在逻辑上已经没什么漏洞了,但仍然有优化的空间。因为这样写,每次调用getInstance()方法时都会受到同步锁synchronized的影响。而事实上,这个同步锁是对性能有消耗的。如果instance已经实例化了,我们就不需要再获取同步锁了,从而提高性能。

首先把synchronized从方法声明中移到方法体中,这样的效果与上面的完全一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singleton{
private static Singleton instance = null;

private Singleton(){}

public static Singleton getInstance(){
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
}

接着在synchronized外面再加一层判断,只有在没有初始化时,才会获取同步锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Singleton {
private static Singleton instance = null;

private Singleton(){
}

public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}

三 静态内部类

因为JVM在进行类加载的时候会保证数据同步的,所以我们可以采用静态内部类来保证线程安全。

1
2
3
4
5
6
7
8
9
10
11
public class Singleton{
private static class SingletonHolder{
public static Singleton instance = new Singleton();
}

private Singleton(){}

public static Singleton getInstance(){
return SingletonHolder.instance;
}
}

这样写十分简洁,而且同时解决了线程安全和延迟加载的问题。

四 枚举

《Effective Java》中推荐了一种更简洁的方式来实现单例,它就是枚举。

创建枚举实例是线程安全的,所以不需要额外做并发处理。

1
2
3
4
5
6
7
public enum Singleton{
INSTANCE;

public void functionA(){
// 功能方法
}
}

调用方法时可以直接这么用。

1
Singleton.INSTANCE.functionA();

要注意的是,在Android中并不十分推荐使用这种方法。因为它虽然简洁,但枚举内存占用是静态变量的两倍以上,所以尽可能地去避免使用枚举。

总结

好了,设计模式的第一篇,单例模式差不多就讲到这里了。最后再归纳一下单例模式的优缺点。

优点

  • 单例模式在内存中只有一个实例,减少了内存开支。当一个对象需要频繁地创建、销毁,或者对象的产生需要比较多的资源时,单例的优势就非常明显。

  • 单例模式可以避免对资源的多重占用。比如一个写文件操作,由于只有一个实例,可以避免对资源文件的同时操作。

  • 单例模式可以在系统设置全局的访问点,优化和共享资源访问。比如负责所有数据表的映射处理的单例类。

缺点

  • 单例模式一般没有接口(因为它要求自行实例化,接口对它没有意义),所以扩展很困难。

  • 单例模式如果持有Context,很容易引发内存泄漏。要注意传递给单例对象的Context最好是Application Context。

  • 单例模式与单一职责原则有冲突。

单例模式:确保一个类仅有一个实例,而且自行实例化并向整个系统提供这个实例

Chen wechat
欢迎扫描二维码,订阅我的博客公众号MiracleChen