单例模式

单例模式

优点

1
2
3
4
减少内存开支
减少系统性能开销(建立销毁)
避免对资源的多重占用,例如写文件操作,由于只有一个实例在内存中,避免了同时写操作。
可以在系统设置全局的访问点,优化共享访问,例如可以设计一个类,负责所数据表的映射处理。

缺点

1
2
3
4
无接口,难拓展。
对测试不利
与单一职责冲突
高并发的时候就会出现线程同步问题,所以单例模式的实现方式有很多。

一、单例模式简介

1.定义

确保一个类有且只有一个实例,避免产生多个对象消耗过多的资源,或者某种类型的对象只应该有且只有一个,并提供一个访问他的全局访问点

2.为什么要用单例

在我们的系统中,有一些对象其实我们只需要一个,比如说:线程池、缓存、对话框、注册表、日志对象、充当打印机、显卡等设备驱动程序的对象。事实上,这一类对象只能有一个实例,如果制造出多个实例就可能会导致一些问题的产生,比如:程序的行为异常、资源使用过量、或者不一致性的结果。

简单来说使用单例模式可以带来下面几个好处:

  • 对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销;
  • 由于 new 操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻 GC 压力,缩短 GC 停顿时间。

3.为什么不使用全局变量确保一个类只有一个实例呢?

我们知道全局变量分为静态变量和实例变量,静态变量也可以保证该类的实例只存在一个。

只要程序加载了类的字节码,不用创建任何实例对象,静态变量就会被分配空间,静态变量就可以被使用了。

但是,如果说这个对象非常消耗资源,而且程序某次的执行中一直没用,这样就造成了资源的浪费。利用单例模式的话,我们就可以实现在需要使用时才创建对象,这样就避免了不必要的资源浪费。 不仅仅是因为这个原因,在程序中我们要尽量避免全局变量的使用,大量使用全局变量给程序的调试、维护等带来困难。

二、单例模式的实现

通常有两种实现模式,饿汉式和懒汉式

  • 饿汉式:在类加载的时候就实现了类的实例化操作
  • 懒汉式:在类第一次被调用的时候实现类的实例化操作

不管是那种创建方式,它们通常都存在下面几点相似处:

  • 单例类必须要有一个 private 访问级别的构造函数,只有这样,才能确保单例不会在系统中的其他代码内被实例化;
  • instance 成员变量和 getInstance 方法必须是 static 的。

1.饿汉式(线程安全)

1
2
3
4
5
6
7
8
9
10
11
public class Singleton {
//使用 static 修饰,保证在类加载时就能创建该类实例,保证了线程安全
private static Singleton singleton = new Singleton();
//Singleton 只有一个构造方法,并且被 private 修饰,所以用户无法通过 new 方法去创建实例
private Singleton(){

}
public static Singleton getSingleton(){
return singleton;
}
}
  • 在类加载时就立即创建了该类的实例,不管你用不用,我先将实例创建出来再说。
  • 如果你一直没有调用这个类的实例,就会空间的浪费。
  • 典型的空间换时间,每次调用的时候就不需要再判断了,节省了运行成本。

「饿汉式」是最简单的实现方式,这种实现方式适合那些在初始化时就要用到单例的情况,这种方式简单粗暴,如果单例对象初始化非常快,而且占用内存非常小的时候这种方式是比较合适的,可以直接在应用启动时加载并初始化。

但是,如果单例初始化的操作耗时比较长而应用对于启动速度又有要求,或者单例的占用内存比较大,再或者单例只是在某个特定场景的情况下才会被使用,而一般情况下是不会使用时,使用「饿汉式」的单例模式就是不合适的,这时候就需要用到「懒汉式」的方式去按需延迟加载单例。

2.懒汉式(非线程安全)

1
2
3
4
5
6
7
8
9
10
11
public class Singleton {
private static Singleton instance;
private Singleton(){
}
public static Singleton getInstance(){
if (instance == null){
return instance = new Singleton();
}
return instance;
}
}
  • 实例是在第一次被使用时创建,而不是在 JVM 在加载这个类时就马上创建此唯一的实例。
  • 在一定程度上节省了资源,延缓了实例创建的时间
  • 非线程安全

懒汉模式申明了一个静态对象,在用户第一次调用时初始化,虽然节约了资源,但第一次加载时需要实例化,反映稍慢一些,而且在多线程不能正常工作。在多线程访问的时候,很可能会造成多次实例化,就不再是单例了。

「懒汉式」与「饿汉式」的最大区别就是将单例的初始化操作,延迟到需要的时候才进行,这样做在某些场合中有很大用处。比如某个单例用的次数不是很多,但是这个单例提供的功能又非常复杂,而且加载和初始化要消耗大量的资源,这个时候使用「懒汉式」就是非常不错的选择。

3.懒汉式(线程安全)

上面有说到,在多线程下懒汉式不能正常工作,为了保证线程安全,需要在 getInstance() 方法前加上 synchronized 关键字,如下:

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

private static Singleton instance = null;
private Singleton(){
}
public static synchronized Singleton getInstance(){
//判断当前实例是否存在,如果不存在,则创建实例,
if (instance == null){
return instance = new Singleton();
}
return instance;
}
}

这两种「懒汉式」单例,名字起的也很贴切,一直等到对象实例化的时候才会创建,确实够懒,不用鞭子抽就不知道走了,典型的时间换空间,每次获取实例的时候才会判断,看是否需要创建,浪费判断时间,如果一直没有被使用,就不会被创建,节省空间。

因为这种方式在getInstance()方法上加了同步锁,所以在程序中每次使用 getInstance() 都要经过 synchronized 加锁这一层,这难免会增加 getInstance() 的方法的时间消费,在多线程情况下会造成线程阻塞,把大量的线程锁在外面,只有一个线程执行完毕才会执行下一个线程。

我们下面介绍到的 双重检查加锁版本 就是为了解决这个问题而存在的。

4.懒汉式(双重检查加锁版本)

利用双重检查加锁(double-checked locking),首先检查是否实例已经创建,如果尚未创建,“才”进行同步。这样以来,只有一次同步,这正是我们想要的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Singleton {
//volatile 保证,当 instance 变量被初始化成 Singleton 实例时,多个线程可以正确处理instance 变量
private static volatile Singleton instance;
private Singleton(){

}
public static Singleton getInstance(){
//检查实例,如果不存在,就进入同步代码块
if (instance == null){
//只有第一次才彻底执行这里的代码
synchronized (Singleton.class){
//进入同步代码块后,在检查一次,如果依然为空,那么就创建实例
if (instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}

这种写法在getSingleton()方法中对 singleton 进行了两次判空

​ 第一次是为了不必要的同步,先判断 instance 是否为 null:如果不为 null,说明 instance 已经被初始化了,直接返回 instance;如果 instance 为 null,说明 instance 还没有被初始化,这样才会去执行 synchronized 修饰的代码块的内容,只在其初始化的时候调用一次。这样的设计既能保证只产生一个实例,并且只在初始化的时候加同步锁,也实现了延迟加载。

为什么要用 volatile 修饰?

因为 singleton = new Singleton()并不是一个原子操作。在执行 new 时,JVM 会发生以下几个动作:

1
2
3
memory = allocate();   //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance = memory; //3:设置instance指向刚分配的内存地址

当第三步完成之后,对象就不再是 null 了。

但是 JVM 会进行指令的优化重排序,将原本1-2-3的顺序可能变为1-3-2的顺序。

1
2
3
4
memory = allocate();   //1:分配对象的内存空间  
instance = memory; //3:设置instance指向刚分配的内存地址
//注意:此时对象还没有被初始化!
ctorInstance(memory); //2:初始化对象

将第2步和第3步调换顺序,在单线程情况下不会影响程序执行的结果,但是在多线程情况下就不一样了。

这里需要明确的一点是:对于 synchronized 关键字,当一个线程访问对象的一个 synchronized(xx.class) 同步代码块时,另一个线程仍然可以访问该对象中的非 synchronized(xx.class) 同步代码块。

线程A执行了

1
memory = allocate();//这对另一个线程B来说是可见的

此时线程B执行外层

1
2
3
if (instance == null){
...
}

发现 instance 不为空,随机返回,但是得到的却是未被完全初始化的实例,在使用时必会有一定的风险,因此使用 volatile 修饰。

关于 volatile 可以参考这里

「双重校验锁」:既可以达到线程安全,也可以使性能不受很大的影响,换句话说在保证线程安全的前提下,既节省空间也节省了时间,集合了「饿汉式」和两种「懒汉式」的优点,取其精华,去其槽粕。

对于volatile关键字,还是存在很多争议的。由于volatile关键字可能会屏蔽掉虚拟机中一些必要的代码优化,所以运行效率并不是很高。也就是说,虽然可以使用“双重检查加锁”机制来实现线程安全的单例,但并不建议大量采用,可以根据情况来选用。

还有就是在java1.4及以前版本中,很多JVM对于volatile关键字的实现的问题,会导致“双重检查加锁”的失败,因此“双重检查加锁”机制只能用在java1.5及以上的版本。

5.懒汉式(静态内部类)[推荐]

在很多情况下JVM已经为我们提供了同步控制,比如:

  • 在 static{…} 块中初始化的数据
  • 访问 final 字段时

因为在 JVM 进行类加载的时候他会保证数据是同步的,我们可以这样实现:采用内部类,在这个内部类里面去创建对象实例。这样的话,只要应用中不使用内部类,JVM 就不会去加载这个单例类,也就不会创建单例对象,从而实现「懒汉式」的延迟加载和线程安全。

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

private Singleton(){

}

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

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

}

第一次加载 Singleton 类时并不会初始化 instance,只有第一次调用 getInstance 方法时虚拟机加载 SingletonHolder 并初始化 instance,这样不仅能确保线程安全也能保证 Singleton 类的唯一性,所以推荐使用静态内部类单例模式。

然而,这还不是最简单的方式,《Effective Java》中作者推荐了一种更简洁方便的使用方式,就是使用「枚举」。

6.枚举

这种方法在功能上与公有域方法相近,但是它更加简洁,无偿提供了序列化机制,绝对防止多次实例化,即使是在面对复杂序列化或者反射攻击的时候。虽然这种方法还没有广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。 —-《Effective Java 中文版 第二版》

1
2
3
4
5
6
public enum Singleton {
INGLETON;
public void dosomething(){
System.out.println("枚举方式实现单例");
}
}

使用方法如下:

1
2
3
4
5
6
public class Main {
public static void main(String[] args) {
Singleton singleton = Singleton.INGLETON;
singleton.dosomething();
}
}

枚举单例的优点就是简单,并且默认枚举实例的创建是线程安全的,但是单例这样用的人我觉得还是不多,可读性并不是很高,不建议大家使用。猜想是大家对枚举了解不太多吧。如果看到枚举这个方式一脸懵B,就看看枚举相关的知识。反正我一开始也是一脸懵B

7.使用容器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class SingletonManager {
private static Map<String,Object> objMap = new HashMap<String,Object>();
private SingletonManager(){

}
public static void registerService(String key, Object instance){
if (!objMap.containsKey(key)){
objMap.put(key,instance);
}
}

public static Object getService(String key) {
return objMap.get(key);
}
}

用 SingletonManager 将多种的单例类统一管理,在使用时根据 key 获取对象对应类型的对象。

这种方式的优点在于

  • 可以管理多种类型的单例
  • 在使用时可以通过统一的接口进行获取操作,降低了用户的使用成本
  • 对用户隐藏了具体实现,降低了耦合度

小结

主要介绍了7种单例模式的使用方式,但是无论采用哪种方式,都应该根据项目本身来决定!

-------------本文结束感谢您的阅读-------------

本文标题:单例模式

文章作者:Cui Zhe

发布时间:2018年12月03日 - 23:12

最后更新:2018年12月03日 - 23:12

原始链接:https://cuizhe1023.github.io/2018/12/03/单例模式/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。