0%

设计模式读书笔记-单例模式

定义

单例模式:确保某一个类只有一个实例,且只能自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。

适用场景

  1. 系统只需要一个实例对象。例如,系统要求提供一个唯一的序列号生成器或者资源管理器,或者需要考虑资源消耗太大只允许创建一个状态(Windows进程管理器无论点击多少次始终只能弹出一个窗口,因为如果多个窗口弹出意味着在某一瞬间系统资源使用情况和进程,服务等信息存在多个状态)。
  2. 客户端调用类的单个实例只允许一个公共访问类。

单例模式概述

单例类有很多实现方式,但是基本共同要点在于

构建私有构造函数

原因在于为了确保单例实例的唯一性,需要禁止类的外部直接用new来创建对象,因此需要将构造函数的可见性改为private

1
private Singleton(){}

定义静态Sinleton类型的私有成员变量

  • 类的外部不能使用new来创建对象,但是外部能访问这个唯一实例,同时内部能创建保存这个唯一实例。
  • 为什么成员变量需要定义为静态?因为提供给外部的静态方法不能访问非静态变量。
1
private static Singleton single = null;

定义一个公有的静态方法

提供给外界使用并实例化成员变量

1
2
3
4
5
6
7
public static Singleton getInstance(){
if (sinlge == null){
single = new Singleton();
}

return tm;
}

因此最简单经典的单例模式如下

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

}

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

这是最经典实现方式,结合项目使用情况衍生出更多的实现方式

  • Eager Singleton
  • Lazy and Thread safe Singleton
  • Bill Pugh Singleton
  • Enum Singleton

饿汉单例

结构图如下
EagerSingleton
由于在定义静态变量时实例化单例类,因此在类加载的时候就已经创建了单例对象,可确保单例对象的唯一性。

1
2
3
4
5
6
7
8
9
10
public class EagerSingleton{
private static final EagerSingleton single = new EagerSingleton();

private EagerSinlgeton(){
}

public static EagerSingleton getInstance(){
return single;
}
}

懒汉单例与线程安全

结构图如下
LazySingleton
可以看到懒汉单例在第一次调用getInstance()时实例化,在类加载时并不自行实例化,这种技术又称为「延迟加载(Lazy Load)」技术,即需要的时候再加载实例。为了避免多个线程同时调用getInstance()方法,可以使用synchronized关键字。

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

private LazySingleton(){

}

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

这样虽然解决了线程安全问题,但是每次调用getInstance()时都需要进行线程锁定判断,在多线程高并发访问环境中,将会导致系统性能大大降低。综合考虑,不用对整个getInstacne()进行锁定,只需锁定single = new Singleton();即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class LazySingleton{
private static LazySingleton sinlge = null;

private LazySinlgeton(){

}

//the fisrt
public volatile static LazySingleton getInstacne(){
if (single == null){
synchronized(LazySingleton.class){
single = new LazySinleton();
}
}
return single;
}

//the second
public volatile static LazySingleton getInstance(){
if (single == null){
synchronized(LazySingleton.class){
if (single == null){
single = new Singleton();
}
}
}
return single;
}
}

第二种比第一种方法多了一层if (single == null)判断,为什么需要这层判断?不加可不可以?

答案是不行。

如果使用第一种方法来创建单例对象,还是会存在单例对象不唯一。

因为假如A线程和B线程在同一时间调用getInstance()方法,此时均为null值,均能通过single == nulli判断。此时A线程进入synchronized锁定的代码模块执行实例创建,B线程处于排队等待状态,必须等待A线程执行完才能进入synchronized锁定代码模块。但当A线程执行完毕后,B线程并不知道实例已经创建,会再次创建实例导致产生多个实例对象,依然无法保证唯一的单例对象,因此需要再加入一层判断,这种方式称为「双重检查锁定(Double-Check Locking)


但是在JDK1.5之前依然存在DCL失效问题。

single = new Singleton()语句,这里看起来是一段代码,但实际上它并不是一个原子操作,这句代码最终会被编译成多条汇编指令,大致做了3件事情:

  1. 给Singleton的实例分配内存
  2. 调用Singletion()的构造函数,初始化成员字段
  3. 将single对象指向分配的内存空间(此时single就不是null了)

但是,由于Java编译器允许处理器乱序执行,以及JDK1.5之前JVM中Cache,寄存器到主内存回写顺序的规定,上面的第二和第三的顺序是无法保证的。也就是说,执行顺序有可能是1-2-3,也有可能是1-3-2。如果是后者,并在3执行完毕、2未执行之前,被切换到B线程,这时候single因为已经在线程A内执行过了第三点,single已经是非空了,所以,线程B直接取走single,再使用就会出错,这就是DCL失效问题

在JDK1.5之后,官方注意到这种问题,调整了JVM,具体化了volatile关键字,因此在1.5之后的版本,将single的定义为private volatile static Singleton single = null就可以保证single对象每次都是从主存中读取,禁止指令重排序优化,但同时volatile关键字会屏蔽JVM所做的一些代码优化,可能会导致系统运行效率降低。

Bill Pugh Singleton

Prior to Java 5, java memory model had a lot of issues and above approaches used to fail in certain scenarios where too many threads try to get the instance of the Singleton class simultaneously. So Bill Pugh came up with a different approach to create the Singleton class using a inner static helper class. The Bill Pugh Singleton implementation goes like this;

1
2
3
4
5
6
7
8
9
10
11
12
13
public class BillPughSingleton{
private BillPughSingleton(){

}

private static class SingletonHelper{
private final static BillPughSingleton instacne = new BillPughSingleton();
}

public static BillPughSingleton getInstance(){
return SingletonHelper.instance;
}
}

由于静态单例对象没有作为BillPushSingleton的成员变量直接实例化,因此类加载时不会实例化Singleton,第一次调用getInstance()时将加载内部类SinlgetonHelper,内部类中的static变量instance会首先被初始化,由JVM来保证其线程安全,确保该成员变量只能初始化一次。

可是为什么这样就是线程安全的呢?很多文章和书籍都没有解释清楚都是一笔带过,JVM保证线程安全。

首先要知道的是静态变量只会初始化一次当类加载时

其次是Java是多线程编程,初始化类或者接口时需要很精细的同步操作,因为同一时间可能有很多其他的线程尝试去初始化同样的类或接口

For each class or interface C, there is a unique initialization lock LC. The mapping from C to LC is left to the discretion of the Java Virtual Machine implementation.

首先需要明确的是当getInstance()调用时才会加载SingletonHelper类,
而加载SingletonHelper类时会执行private final static Singleton instance = new Singleton();生成Singleton实例

简单来说,假如当两个线程尝试去初始化getInstance()时,就会去加载SingletonHepler类,这时需要LC锁的第一个线程才是实际上去初始化getInstance()的线程能加载SingletonHelper类的线程,因为instance是静态初始化,Java保证它只能被初始化一次。而当其他线程再进入getInstance()时,系统加载执行早已结束,保证了线程安全。

贴上两个摘要便于理解

  • Java Concurrency in Practice

    The lazy initialization holder class idiom uses a class whose only purpose is to initialize the Resource. The JVM defers initializing the ResourceHolder class until it is actually used [JLS 12.4.1], and because the Resource is initialized with a static initializer, no additional synchronization is needed. The first call to getresource by any thread causes ResourceHolder to be loaded and initialized, at which time the initialization of the Resource happens through the static initializer.

  • Static initialization

    Static initializers are run by the JVM at class initialization time, after class loading but before the class is used by any thread. Because the JVM acquires a lock during initialization [JLS 12.4.2] and this lock is acquired by each thread at least once to ensure that the class has been loaded, memory writes made during static initialization are automatically visible to all threads. Thus statically initialized objects require no explicit synchronization either during construction or when being referenced.

枚举单例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public enum Singleton{
INSTANCE;

private Sinlgeton(){
}

public void doWorks(){
//do something here
}
}

//If you then added another class with a main() method like
public static void main(String[] args){
Sinlgeton.INSTANCE.doWorks();
}

首先要明确的是枚举是一种特殊的类

因此上文的枚举声明实际上可以类比为这样

1
2
3
public class Singleton{
public final static Singleton INSATNCE = new Singleton();
}

当首次去调用INSTANCE时,Singleton类会被JVM加载并初始化,同时初始化静态变量的过程只发生一次,实现了延迟加载(lazily)。

其次枚举单例能够自行处理序列化,传统的单例一旦实现序列化接口将不能保证实例的唯一性,因为readObject()方法总是能像构造器那样返回一个新的实例。但是可以通过readResolve()方法避免这种情况发生。

1
2
3
4
//readResolve to prevent another instance of Singleton
private Object readResolve(){
return INSTANCE;
}

但实际上会更复杂运用在自己的单例类里,但是使用枚举类型,JVM来保证序列化。

最后就是创建枚举实例默认是线程安全的(恩不需要解释就是默认)以及简单易写相比于上面几种单例。

VS

Implementation Advantage Disadvantage
Eager Sinlgeton 1.无需考虑多线程访问问题,可以确保实例的唯一性 2.保持较高的系统性能 类加载时就创建对象,不管将来用不用,始终占据内存,资源利用效率低
Lazy and Thread safe Singleton 实现延迟加载,无须一直占用系统资源 线程安全控制繁琐,而且系统性能受影响
Bill Pugh Singleton 实现延迟加载,又可以保证线程安全,不影响系统性能 与编程语言本身特性相关,很多面向对象语言不支持
Enum Singleton 实现延迟加载,又可以保证线程安全,不影响系统性能,同时支持序列化,简答易写 扩展性差

单例模式总结

主要优点

  1. 单例模式提供对唯一实例的受控访问,严格控制怎么以及何时访问唯一实例。
  2. 保证只存在唯一实例,节约系统资源。
  3. 允许可变数目的实例。

主要缺点

  1. 单例模式中缺少抽象层,因此单例类的扩展性很差。
  2. 单例类的职责过重,在一定程度上违背了单一职责原则。单例类既提供了业务方法,也提供了创建对象的方法(工厂方法),将对象的创建和对象本身的功能耦合在一起。
  3. 如今很多面向对象语言都提供自动垃圾回收技术,如果实例化的共享对象长时间不被利用,系统会认为它是垃圾,会自动销毁回收资源,下次利用时需要重新实例化。

留坑

留个volatile的坑后面来填~

相关链接:

Java Singleton with an inner class - what guarantees thread safety?

Implementing Singleton with an Enum

Volatile Vs Static

Java Volatile Keyword