【设计模式】单例模式

设计模式-单例模式的七种写法-Java实现

Posted by yangkai on 2018-04-01

整体介绍


介绍

  • 单例模式(Singleton Pattern) 是 Java 中最简单的设计模式之一。
  • 这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
  • 意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

  • 主要解决:一个全局使用的类频繁地创建与销毁。
  • 何时使用:当您想控制实例数目,节省系统资源的时候。
  • 如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
  • 关键代码:构造函数是私有的。

优缺点

优点:

  1. 减少内存开销;尤其是频繁的创建和销毁实例,在内存里只有一个实例,可以减少开销。
  2. 避免资源占用;比如写文件操作,避免对资源的多重占用。

缺点:

  1. 没有接口,不能继承,与单一职责原则冲突。
  2. 一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。

使用场景

  1. 取号器;要求生产唯一序列号。
  2. 计数器;WEB中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
  3. 连接池;创建的一个对象需要消耗的资源过多,比如 I/O 和数据库的连接等。

注意事项
getInstance() 方法中需要使用同步锁 synchronized (Singleton.class) 防止多线程同时进入造成 instance 被多次实例化。

写法简述


7种实现方式

  1. 懒汉式
  2. 懒汉式,线程安全
  3. 懒汉式,双重检验锁
  4. 饿汉式
  5. 饿汉式,变种
  6. 静态内部类
  7. 枚举
  • 不过一般来说,第一种不算单例
  • 第四种和第五种就是一种,如果算的话,第五种也可以分开写了。
    所以说,一般单例都是五种写法。懒汉、饿汉、双重校验锁、静态内部类和枚举。

经验之谈

  • 一般情况下,不建议使用"懒汉式"和"懒汉式,线程安全"这两种方式;建议使用"饿汉式"的方式。
  • 只有在要明确实现懒加载效果时,才会使用"静态内部类"的方式。
  • 如果涉及到反序列化创建对象时,可以尝试使用"枚举"的方式。
  • 如果有其他特殊的需求,可以考虑使用"懒汉式,双重检验锁"的方式。

实现方式


懒汉式

优缺点

优点:

  • 懒加载;延迟加载,即用时才加载
  • 容易实现

缺点:

  • 线程不安全;没有加锁,严格意义上它并不算单例模式

代码实现

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

/* 持有私有静态实例,防止被引用,此处赋值为null,目的是实现延迟加载 */
private static Singleton instance = null;
/* 私有构造方法,防止被实例化 */
private Singleton() {
}
/* 提供静态方法,供外部获取实例 */
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

懒汉式,线程安全

优缺点

优点:

  • 懒加载;延迟加载,即用时才加载
  • 容易实现
  • 线程安全;使用synchronized加锁

缺点:

  • 使用synchronized加锁;影响效率

代码实现

1
2
3
4
5
6
7
8
9
10
public class Singleton {    

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

懒汉式,双重检验锁

优缺点

优点:

  • 懒加载;延迟加载,即用时才加载
  • 线程安全;效率高

缺点:

  • 实现困难,潜在问题很多,后面分析

代码实现

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

private static Singleton instance = null;
// 只在第一次初始化的时候加上同步锁*/
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}

潜在问题分析

这种方法貌似很完美的解决了上述效率的问题,它或许在并发量不多,安全性不太高的情况能完美运行,但是,这种方法也有不幸的地方。问题就是出现在这句 instance = new Singleton();
在JVM编译的过程中会出现指令重排的优化过程,这就会导致当instance实际上还没初始化,就可能被分配了内存空间,也就是说会出现 instance !=null 但是又没初始化的情况,这样就会导致返回的 instance 不完整。
我们来看看这个场景:假设线程一执行到instance = new SingletonKerrigan()这句,这里看起来是一句话,但实际上它并不是一个原子操作,这句话被编译成8条汇编指令,大致做了3件事情:

  1. 给实例分配内存。
  2. 初始化构造器
  3. 将instance对象指向分配的内存空间(注意到这步instance就非null了)。
    但是,由于Java编译器允许处理器乱序执行(out-of-order),以及JDK1.5之前JMM(Java Memory Medel)中Cache、寄存器到主内存回写顺序的规定,上面的第二点和第三点的顺序是无法保证的,也就是说,执行顺序可能是1-2-3也可能是 1-3-2,如果是后者,并且在3执行完毕、2未执行之前,被切换到线程二上,这时候instance因为已经在线程一内执行过了第三 点,instance已经是非空了,所以线程二直接拿走instance,然后使用,然后顺理成章地报错,而且这种难以跟踪难以重现的错误很难找得出来。
    在JDK1.5之后,官方已经注意到这种问题,因此调整了JMM、具体化了volatile关键字, 因此如果JDK是1.5或之后的版本,只需要将instance的定义改成“private volatile static SingletonKerriganD instance = null;”就可以保证每次都去instance都从主内存读取。当然volatile或多或少也会影响到性能。

饿汉式

优缺点

优点:

  • 容易实现
  • 线程安全

缺点:

  • 实时加载,第一时间被创建;性能有缺陷
  • 某些场景无法使用;譬如实例的创建是依赖参数或者配置文件的,在getInstance()之前必须调用某个方法设置参数给它。

代码实现

1
2
3
4
5
6
7
public class Singleton {    

private static SingletonD instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
}

饿汉式,变种

优缺点

优点:

  • 容易实现
  • 线程安全

缺点:

  • 非懒加载,类创建就被创建
  • 某些场景无法使用;同第三种一样

代码实现

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

静态内部类

优缺点

优点:

  • 懒加载;内部私有类,调用getInstance才会访问,才会加载创建。
  • 线程安全;能达到双检锁方式一样的功效。
  • 实现起来难度不大,中等难度

代码实现

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

private static class SingletonHolder {
private static Singleton instance = new Singleton();
}
/**
* 私有的构造函数
*/
private Singleton() {

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

枚举

优缺点

优点:

  • 懒加载;性能高
  • 容易实现
  • 线程安全
  • 单例实现的最佳方式,代码简洁

这种方式是Effective Java作者Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。

代码实现

1
2
3
4
5
public enum SingletonEnum {  
instance;
SingletonEnum() {
}
}

创建单例实例的方式


直接new单例对象

一般我们会加入一个private或者protected的构造函数,这样系统就不会自动添加那个public的构造函数了,因此只能调用里面的static方法,无法通过new创建对象。

通过反射构造单例对象

如果单例由不同的类装载器装入,那便有可能存在多个单例类的实例。
假定不是远端存取,例如一些servlet容器对每个servlet使用完全不同的类装载器,这样的话如果有两个servlet访问一个单例类,它们就都会有各自的实例。

修复的办法是:

1
2
3
4
5
6
7
8
private static Class getClass(String classname) throws ClassNotFoundException {   

ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
if(classLoader == null)
classLoader = Singleton.class.getClassLoader();
return (classLoader.loadClass(classname));
}
}

通过序列化构造单例对象

如果单例对象有必要实现Serializable接口(很少出现),则应当同时实现readResolve()方法来保证反序列化的时候得到原来的对象。

写法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Singleton implements Serializable {   

private static class SingletonHolder {
static final Singleton INSTANCE = new Singleton();
}

public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
/**
* private的构造函数用于避免外界直接使用new来实例化对象
*/
private Singleton() {
}

/**
* readResolve方法应对单例对象被序列化时候
*/
private Object readResolve() {
return getInstance();
}
}