设计模式之一——单例模式
1. 单例模式
1.1 定义
什么是单例模式,单例模式(Singleton)又叫单态模式,他出现的目的是为了保证一个类在系统中只有一个实例,并提供一个访问它的全局访问点。
许多时候整个系统只需要拥有⼀个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在⼀个文件中,这些配置数据由⼀个单例对象统⼀读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种⽅式简化了在复杂环境下的配置管理。
单例模式拥有以下几个要素
- 私有的构造方法(保证在其他地方的代码无法实例化该对象,只有通过该类提供的静态方法来得到该类的唯一实例)
- 指向自己实例的私有静态引用(在该类内提供一个静态方法,当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用)
- 以自己的实例为返回值的静态的共有的方法。(一个类有且只有一个实例,并且自行实例化向整个系统提供)
特点
- 类的构造函数一般用 private 修饰,不对外公开
- 一般通过一个静态方法返回单例对象
- 必须保证线程安全,即在多线程场景下能确保只有一个单例对象
怎样确保某一个类只有一个实例
- 私有化空构造方法,避免多处实例化。
- 自行实例化,保证实例化在内存中只存在一份。
- 提供共有的静态gentlnstance()方法,并将单一实例返回。
1和3是固定的单例模式套路,基本不会有变。
2则有很多灵活的实现方式,只要保证只实例化一次就可以。
基本代码:
public class DataSourceSingleton { // 1.提供私有的构造方法 private DataSourceSingleton() { } // 2. 创建一个私有的属性对象 private static DataSourceSingleton dataSource = new DataSourceSingleton(); // 3.提供公共的对外的单例对象 public static DataSourceSingleton getInstance() { return dataSource; }}
1.2 为什么要用单例模式
在我们的系统中,有一些对象其实我们只需要一个,比如说:线程池、缓存、对话框、打印机、显卡等设备驱动程序的对象。事实上,这一类对象只能有一个实例,如果制造出多个实例就可能会导致一些问题的产生,比如:程序的行为异常、资源使用过量、或者不一致性的结果。
简单来说:
- 对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销。
- 由于new操作次数的减少,因而对系统内存的使用频率也会降低,这将减轻 GC 压力,缩短 GC 停顿时间。
1.3 为什么不使用全局变量确保一个类只有一个实例呢?
我们直到全局变量分为静态变量和实例变量,静态变量也可以保证该类的实例只存在一个。只要程序加载了类的字节码,不用创建任何实例对象,静态变量就会被分配空间,静态变量就可以被使用了。
但是,如果说这个对象非常消耗资源,而且程序某次的执行中一直没用,这样就造成了资源的浪费。例如单例模式的话,我们就可以实现在需要使用时才创建对象,这样就避免了不必要的资源浪费。不仅仅是因为这个原因,在程序中我们要尽量避免全局变量的使用,大量使用全局变量给程序调试、维护等带来困难。
2. 单例模式实现
2.1 懒汉式单例模示
所谓 “懒汉方式” 就是说单例模式再第一次使用时创建,而不是在JVM在加载这个类时马上创建此唯一的单例实现
为什么叫懒汉?
因为懒汉懒惰,懒得初始化,用到了才开始初始化。
这也就会造成线程安全问题,以下是对懒汉方式创建单例模式的线程安全问题剖析。
懒汉模式代码:
public class DataSourceSingleton2 { // 私有的构造方法 private DataSourceSingleton2() { // ② } // 私有属性 private static volatile DataSourceSingleton2 dataSource = null;// ① // 公共的访问方法,得到单例对象 public static DataSourceSingleton2 getInstance() { if(dataSource == null) { // 大致分流执行 ③ synchronized (DataSourceSingleton2.class) { // 排队执行 /** +1 */ if(dataSource == null) { // 到这里就只有一个可以被实例化了 ④ dataSource = new DataSourceSingleton2(); // ⑤ } } }return dataSource; }
以上代码是一个极为精简且十分合格的单例模式的代码,下面我们来解读一下:
(请大家根据以上代码注释中的标记来结合看以下解释)
首先,注意到 ① 处的volatile
关键字,它具备两个特征
解决了内存可见性问题,即就是:当一个线程修改了这个公共变量的值,新的值对于其他线程来说是可以立即得知的。
禁止了操作系统的指令重排序
这里主要是由于代码 ⑤ 处dataSource = new DataSourceSingleton2();
的这里的指令重排序问题。因为这个初始化操作并不是原子的,大体可分为如下三步:
- 给变量分配内存
- 调用构造函数初始化成确定的实例
- 让
dataSource
执行分配的内存空间
JVM 允许再保证结果正确的前提下进行指令重排序优化。即如上3步可能的顺序为 1->2->3 或 1->3->2。如果顺序是 1->3->2,当3执行完,2还未执行时,另一个线程执行到代码③处,发现dataSource
不为null
,直接返回还未实例化的dataSource
并使用,此时这个内存空间所存储的内容就会被返回了,但是这里返回的空间还没有接收到我们对他的实例化,就会报错。
所以使用volatile
,就是为了保证线程间的可见性和防止指令重排序。
其次,在代码②处将构造函数声明为private
目的在于阻止在其它类中对此类生成新的实例。
最后,还值得一提的是,懒汉式代码需要使用双重检查锁,即 DCL (Double Check Lock)。那么为什么这样写呢?
有这样一种情况,线程1,2同时判断了第一次为空③,在加锁的地方阻塞了,如果没有第二次判空④,那么线程1 执行完毕后线程2 就会再次执行,这样就初始化了两次,就存在问题了。所以进入Synchronized
临界区以后,还要再做一次判空。因为两个线程同时访问的时候,线程1 构造完对象,线程2 也已经通过了最初的判空验证,不做第二次判断,线程2 还是会再次构造对象。
2.2 饿汉式单例模式
所谓 “饿汉方式” 就是说JVM在加载这个类时就马上创建此唯一的单例实例,不管你用不用,先创建了再说,如果一直没有被使用,使浪费了空间,典型的空间换时间,每次调用的时候,就不需要再判断,节省了运行时间。
为什么叫饿汉?
因为饿汉很饿,需要尽早初始化来喂饱自己。
饿汉式代码实现
public class DataSourceSingleton { // 1.提供私有的构造方法 private DataSourceSingleton() { } // 2. 创建一个私有的属性对象,并直接实例化 private static DataSourceSingleton dataSource = new DataSourceSingleton(); // 3.提供公共的对外的单例对象 public static DataSourceSingleton getInstance() { return dataSource; }}
饿汉模式特点:
- 线程安全:利用类加载器的机制,肯定是线程安全的。
- 优点:类加载时会初始化单例对象,首次调用速度变快
- 缺点:类加载时会初始化单例对象,容易产生垃圾。
2.3 静态内部类单例模式
public class DataSourceSingleton3 { // 提供静态内部类,存在静态单例声明与初始化 private static class DataSourceSingletonHolder { private static DataSourceSingleton3 dataSource = new DataSourceSingleton3(); } // 私有静态单例对象 private DataSourceSingleton3(){ } // 提供静态方法返回静态内部类中的单例对象 public static DataSourceSingleton3 getInstance() { return DataSourceSingletonHolder.dataSource; }}
DataSourceSingletonHolder
是静态内部类,当外部类DataSourceSingleton3
被加载时并不会创建任何实例,只有当DataSourceSingleton3.getInstance()
被调用的时候,才会创建实例,这一切由 JVM 天然完成,所以既保证了线程安全,有实现了延迟加载。
2.4 利用枚举实现单例模式
public enum DataSourceSingleton4 { INSTANCE; public DataSourceSingleton4 getInstance() { return INSTANCE; }}
使用时直接.INSTANCE.getInstance()
即可。
特点:
- 保证只有一个实例
- 线程安全
- 自由序列化
可以说枚举就是一个天生的单例,而且还可以自由序列化,反序列化后也是单例的。
几点补充:
- volatile关键字不但可以防⽌指令重排,也可以保证线程访问的变量值是主内存中的最新值。
- 使⽤枚举实现的单例模式,不但可以防止利用反射强行构建单例对象,而且可以在枚举类对象被反序列化的时候,保证反序列的返回
结果是同⼀对象。 - 对于其他⽅式实现的单例模式,如果既想要做到可序列化,⼜想要反序列化为同⼀对象,则必须实现readResolve方法。