介绍
单例模式(Singleton Pattern)是最简单的设计模式之一,顾名思义就是只有一个实例对象,保证一个类仅有一个实例,并提供一个访问它的全局访问点。
用途
解决一个全局使用的类频繁地创建与销毁问题;控制实例数目,节省系统资源。
应用实例
对于系统中的某些类来说,只有一个实例很重要,例如:
1、一个系统中可以存在多个打印任务,但是只能有一个正在工作的任务;
2、一个系统只能有一个窗口管理器或文件系统;
3、一个系统只能有一个计时工具或ID(序号)生成器。
如在 Windows 中就只能打开一个任务管理器。如果不使用机制对窗口对象进行唯一化,将弹出多个窗口,如果这些窗口显示的内容完全一致,则是重复对象,浪费内存资源;如果这些窗口显示的内容不一致,则意味着在某一瞬间系统有多个状态,与实际不符,也会给用户带来误解,不知道哪一个才是真实的状态。因此有时确保系统中某个对象的唯一性即一个类只能有一个实例非常重要。
核心代码
1、构造函数私有化。
2、类定义中含有一个该类的静态私有对象。
3、提供一个静态共有函数用于获取该唯一对象。
实现方式
1. 懒汉式,线程不安全
class Singleton { // 懒汉式,线程不安全 private static Singleton instance; // 静态私有对象 private Singleton() {} // 私有构造函数 public static Singleton getInstance() { // 静态公有访问函数 if (instance == null) { instance = new Singleton(); } return instance; } }
懒汉式,顾名思义就是比较懒,在调用的时候才去检查有没有实例,如果有则直接返回,没有则新建。这种方式是最基本的实现方式,使用了懒加载模式(lazy loading),但是却存在一个致命问题,就是不支持多线程。当有多个线程并行调用 getInstance() 的时候,就会创建多个实例,也就是说在多线程下不能正常工作。
2、懒汉式,线程安全
class Singleton { // 懒汉式,线程安全 private static Singleton instance; // 静态私有对象 private Singleton() {} // 私有构造函数 public static synchronized Singleton getInstance() { // 静态公有访问函数 if (instance == null) { instance = new Singleton(); } return instance; } }
为了解决线程不安全问题,最简单的方法是为整个 getInstance() 方法加上同步锁(synchronized)。这种方式具有懒加载模式(lazy loading),能够在多线程环境下工作。但是,这种做法效率很低,因为只有在第一次调用时需要使用同步锁,99%的情况是不需要同步的。
3、饿汉式
class Singleton { // 饿汉式 private static final Singleton instance = new Singleton(); // 静态私有对象 private Singleton() {} // 私有构造函数 public static Singleton getInstance() { // 静态公有访问函数 return instance; } }
饿汉式,顾名思义就是比较饿,无论是否需要,在调用之前就先实例化对象,可见它不是一种懒加载模式(lazy loading)。这样的好处是没有线程安全问题,不需要同步锁,执行效率会提高。缺点是类加载时就初始化,浪费内存空间。而且在一些场景中将无法使用,例如 Singleton 实例的创建依赖参数或者配置文件,在 getInstance() 之前必须调用某个方法设置参数给它,那样这种单例写法就无法使用了。
4、双检锁/双重校验锁(DCL,即 double-checked locking)
class Singleton { // 双检锁 private volatile static Singleton instance; // 静态私有对象 private Singleton() {} // 私有构造函数 public static Singleton getInstance() { // 静态公有访问函数 if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }
双检锁,又叫双重校验锁,之所以叫它双重校验锁,是因为有两次检查 if(instance == null),一次是在同步块外,一次是在同步块内。为什么在同步块内还要再检验一次? 因为可能会有多个线程一起进入同步块外的 if,如果在同步块内不进行二次检验的话就会生成多个实例了。这样既使用了懒加载(lazy loading),又保证了线程安全,比直接上锁提高了执行效率,还节省了内存空间。
上面代码实现中,特点是在 synchronized 关键字内外都加了一层 if 条件判断,以及 volatile关键字 。两次 if(instance == null)已经解释清楚了,为什么要加 volatile 关键字呢?能否去掉?
有没有 volatile 好像没有什么影响,事实真是这样吗。我们先来看一下 instance = new Singleton() 的执行过程吧。
1、给 Singleton 对象分配内存。
2、调用 Singleton 的构造函数来初始化。
3、将 instance 对象指向分配的内存空间。
如果按照这样进行下去,不加 volatile 关键字好像也没问题,但是在实际执行中存在指令重排序的优化。也就是说上面的步骤 2 和步骤 3 的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2 。如果是后者,在 3 执行完毕,2 还未执行完毕之前被另一线程抢占,此时 instance != null (但却还未初始化),该线程返回 instance ,造成错误。
解决方法很简单,就是加上volatile关键字,它有两层语义:
1、保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2、禁止进行指令重排序。
禁止指令重排序优化。简单来说,就是 volatile 变量的赋值操作中写操作都先行发生于后面对这个变量的读操作。(注意在Java 5以前的版本使用了volatile的双检锁还是有问题的,原因是Java 5以前的Java 内存模型是存在缺陷的)。
5、静态内部类
class Singleton { // 静态内部类 private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); // 静态私有对象 } private Singleton() {} // 私有构造函数 public static final Singleton getInstance() { // 静态公有访问函数 return SingletonHolder.INSTANCE; } }
这种方式能达到双检锁方式一样的功效,但实现更简单,这也是《Effective Java》上所推荐的方法。这种方式同样利用了 classloader 机制来保证初始化 instance 时只有一个线程,它跟第 3 种方式不同的是:第 3 种方式只要 Singleton 类被装载了,那么 instance 就会被实例化(没有达到 lazy loading 效果),而这种方式是 Singleton 类被装载了,instance 不一定被初始化。因为 SingletonHolder 类没有被主动使用,只有通过显式调用 getInstance() 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance,因此它是懒加载模式(lazy loading)的。
对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。
6、枚举
public enum Singleton { // 枚举 INSTANCE; }
这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。我们可以通过 Singleton.INSTANCE 来访问实例,这比调用 getInstance() 方法简单多了,而且创建枚举默认就是线程安全的。它更简洁,自动支持序列化机制,绝对防止多次实例化。这种方式也是《Effective Java》提倡的方式。
总结
一般情况下,直接使用饿汉模式就好了,不建议使用前两种懒汉模式,在明确需要使用懒加载模式(lazy loading)时才使用第五种静态内部类方式,涉及到反序列化创建对象时,可以尝试第六种方式,如果有其他特殊的需求,可以考虑使用第四种双检锁方式。
————————————————
版权声明:本文为CSDN博主「Acx7」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。