设计模式整理(2) 单例模式

学习《Android 源码设计模式解析与实践》系列笔记

什么是单例

单例模式是应用最广,也是最容易理解的模式之一。
在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中,应用该模式的一个类只有一个实例。即一个类只有一个对象实例。

定义

《设计模式》(艾迪生维斯理, 1994)中的定义:“保证一个类仅有一个实例,并提供一个访问它的全局访问点。”
Java 中单例模式定义:“一个类有且仅有一个实例,并且自行实例化向整个系统提供。”

使用场景

为了避免某个类创建多个对象而造成资源的消耗,或者是这个类型的对象应该且只有一个。

结构

设计模式整理(2) 单例模式_第1张图片
单例模式 UML 图

单例模式要点:

  1. 构造行数不对外开发,需要设置为 privte;
  2. 单例类自行实例化一个对象;
  3. 外部能通过一个静态方法或者是枚举拿到该单例类对象;
  4. 确保单例类对象唯一。

实现

单例的实现有多重方式:

1. 饿汉模式

public class Singleton {
    private static final Singleton sInstance = new Singleton();
    private Singleton() {}
    public static Singleton getsInstance() {
        return sInstance;
    }
}

饿汉模式因为是赋值的静态变量,所以会在类加载的时候就进行了初始化,也就是这时已经创建好了静态的单例对象。
这个对象是唯一的,也是线程安全的。

缺点:
过早初始化,占用资源。

2. 懒汉模式

public class Singleton {
    private static Singleton sInstance = null;
    private Singleton() {}
    public static synchronized Singleton getsInstance() {
        if (sInstance == null) {
            sInstance = new Singleton();
        }
        return sInstance;
    }
}

懒汉模式在会获取实例的时候才进行初始化,较饿汉模式节约资源。但是为了保证多线程下第一次调用时实例化对象的唯一性,加了 synchronized 关键字,这个在初始化后的同步都是没有必要的,因此会造成不必要的同步开销。

3. 双重校验锁(double check lock)模式

public class Singleton {
    private static Singleton sInstance = null;
    private Singleton() {}
    public static Singleton getsInstance() {
        if (sInstance == null) {
            synchronized (Singleton.class) {
                if (sInstance == null) {
                    sInstance = new Singleton();
                }
            }
        }
        return sInstance;
    }
}

可以看到,双重校验锁模式是在 2 懒汉模式的基础上的优化。
同步锁不再加在外层函数上,而是加在了初始化模块上。这样既保证了线程安全问题,又解决了初始化后同步消耗问题。


这种模式的关键点是在两个判空上面,第一次的判空是为了避免不必要的同步,而第二步是为了确保不被多次实例化。
例如:A 线程和 B 线程同时执行到 synchronized (Singleton.class) 这一步,A 拿到了锁,(B 则会等待 A 执行完后释锁)然后执行了 sInstance = new Singleton()。这时,A 执行完了,并且实例化了 sInstance 了,然后释放锁后 B 拿到了锁,B 继续往下执行,假设这时没有第二次判空,那 B 会再次实例化一个 sInstance 对象,这样 A 和 B 拿到的对象就不是同一个了。所以说第二次的判空是必须的,这时用来保证实例化对象的唯一性的。


然而,两次判空就真的能保证实例化对象的唯一性了吗?
答案是否定的。
在 A 执行到 Instance = new Singleton()时,看似是一句代码,但实际上它不是一个原子操作,这句代码最终会被编译成多条汇编指令(真实淡疼),汇编指令大概做了下面三个操作:
(1) 给 Singeton 的实例分配内存;
(2) 调用 Singleton() 的构造函数,初始化成员字段;
(3) 将 sInstance 对象指先分配的 内存空间(此时 sInstance 就不是 null 了
)。

可以看到,如果是顺序执行的,也是没有问题的,问题是在 JVM 1.5 之前,2,3 步的顺序是不能保证的,也就是可以是 1-2-3的顺序执行,也可能是 1-3-2 的执行顺序。所以,但是如果是后面这种情况,在 A 线程已经执行完 1-3 步时,sInstance 已经非空了,这时 B 线程执行 getInstance 方法发现是非空的,直接拿走 sInstance 使用,就会导致出错。

好的是,JVM 1.5 之后,可以通过 volatile 解决此问题。

双重校验锁模式资源利用率高,但是也同样存在第一次加载反应稍慢的问题。

4. 静态内部类模式

public class Singleton {
    private Singleton() {}
    public static Singleton getsInstance() {
        return Holder.sInstance;
    }

    private static class Holder {
        private static final Singleton sInstance = new Singleton();
    }
}

巧妙利用了虚拟机加载静态内部类的时机和方式,不仅确保了线程安全,也保证实例对象的唯一性。
sInstance 在第一次调用 getInstance 方法时才初始化,所以也是延迟初始化。

这种模式是最推荐的实现方式。

5. 枚举模式

public enum SingletonEnum {
        SINGLETON;
        public void method() {
            ...
        }
    }

枚举类是实现单例的最简单的模式。
枚举和 Java 的普通类一样,能有字段、方法,而且默认枚举实例的创建是线程安全的,并且在任何情况下都是一个单例。

总结

单例模式是设计模式里面最基础的也是使用较为频繁的模式。



相关文章:
设计模式整理(1) 代理模式
设计模式整理(2) 单例模式
设计模式整理(3) Builder 模式
设计模式整理(4) 原型模式
设计模式整理(5) 工厂模式
设计模式整理(6) 策略模式
设计模式整理(7) 状态模式
设计模式整理(8) 责任链模式
设计模式整理(9) 观察者模式
设计模式整理(10) 适配器模式
设计模式整理(11) 装饰模式
设计模式整理(12) 中介者模式

你可能感兴趣的:(设计模式整理(2) 单例模式)