转自:https://blog.csdn.net/zzti_erlie/article/details/80714424
前言
有一些对象其实我们只需要一个,比方说:线程池,缓存,对话框,处理偏好设置和注册表的对象,日志对象,充当打印机,显卡等设备的驱动程序的对象。事实上,这类对象只能有一个实例,如果制造出多个实例,就会导致许多问题产生,例如:程序的行为异常,资源使用过量,或者是不一致的结果
单例模式确保一个类只有一个实例,并提供一个全局访问点,实现单例模式的方法是私有化构造函数,通过 getInstance() 方法实例化对象,并返回这个实例
实现
第一种(懒汉)
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 
 | // code1public class Singleton {
 
 private static Singleton uniqueInstance;
 
 private Singleton() {}
 
 public static Singleton getInstance() {
 if (uniqueInstance == null) {
 uniqueInstance = new Singleton();
 }
 return uniqueInstance;
 }
 }
 
 | 
当2个线程同时进入 getInstance() 的 if 语句里面,会返回 2 个不同实例,因此这种方式是线程不安全的
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 
 | // code2public class Singleton {
 
 private static Singleton uniqueInstance;
 
 private Singleton() {}
 
 public static synchronized Singleton getInstance() {
 if (uniqueInstance == null) {
 uniqueInstance = new Singleton();
 }
 return uniqueInstance;
 }
 }
 
 | 
用 synchronized 修饰可以保证线程安全,但是只有第一次执行此方法时才需要同步,设置好 uniqueInstance,就不需要同步这个方法了,之后每次调用这个方法,同步都是一种累赘
第二种(双重检查锁定)
synchronized 锁的粒度太大,人们就想到通过双重检查锁定来降低同步的开销,下面是实例代码
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 
 | // code3public class Singleton {
 
 private static Singleton uniqueInstance;
 
 private Singleton() {}
 
 public static Singleton getInstance() {
 if (uniqueInstance == null) {
 synchronized (Singleton.class) {
 if (uniqueInstance == null) {
 uniqueInstance = new Singleton();
 }
 }
 }
 return uniqueInstance;
 }
 }
 
 | 
如上面代码所示,如果第一次检查 uniqueInstance 不为 null,那么就不需要执行下面的加锁和初始化操作,可以大幅降低 synchronized 带来的性能开销,只有在多个线程试图在同一时间创建对象时,会通过加锁来保证只有一个线程能创建对象
经常有人对 code3 中,为什么要执行 2 次 if 语句不太清楚,简单描述一下,有可能有 AB 2 个线程同时进入了第一个 if 语句,然后 A 拿到锁,创建对象完成。如果不再做一次判空处理的话,B 拿到锁后会重新创建对象,加了第 2 个 if 语句,就直接退出了
双重检查锁定看起来似乎很完美,但这是一个错误的优化!在线程执行到 getInstance() 方法的第 4 行,代码读取到 uniqueInstance 不为 null 时,uniqueInstance 引用的对象有可能还没有完成初始化
简单概述一下《Java并发编程的艺术》的解释,uniqueInstance = new Singleton() 可以分解为如下三行伪代码
| 12
 3
 
 | memory = allocate();    // 1:分配对象的内存空间ctorInstance(memory);   // 2:初始化对象
 uniqueInstance = memory;// 3:设置uniqueInstance指向刚分配的内存地址
 
 | 
3 行伪代码中的 2 和 3 之间,可能会被重排序,重排序后执行时序如下
| 12
 3
 4
 
 | memory = allocate();    // 1:分配对象的内存空间uniqueInstance = memory;// 3:设置uniqueInstance指向刚分配的内存地址
 // 注意,此时对象还没有被初始化
 ctorInstance(memory);   // 2:初始化对象
 
 | 
多个线程访问时可能出现如下情况
|时间|线程A|线程B|
|—-|—-|—-|
|t1|A1:分配对象的内存空间||
|t2|A3:设置uniqueinstance指向内存空间||
|t3||B1:判断uniqueinstance是否为空|
|t4||B2:由于uniqueinstace不为null,线程B间访问uniqueinstance引用的对象|
|t5|A2:初始化对象||
|t6|A4:访问instace引用的对象||
这样会导致线程 B 访问到一个还未初始化的对象,此时可以用 volatile 来修饰 Singleton,这样 3 行伪代码中的 2 和 3 之间的重排序,在多线程环境中将会被禁止
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 
 | // code4public class Singleton {
 
 private volatile static Singleton uniqueInstance;
 
 private Singleton() {}
 
 public static Singleton getInstance() {
 if (uniqueInstance == null) {
 synchronized (Singleton.class) {
 if (uniqueInstance == null) {
 uniqueInstance = new Singleton();
 }
 }
 }
 return uniqueInstance;
 }
 }
 
 | 
第三种(饿汉)
如果应用程序总是创建并使用单例式例,或者在创建和运行时方面的负担不太繁重,我们可以以饿汉式的方式来创建单例
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 
 | // code5public class Singleton {
 
 private static Singleton uniqueInstance = new Singleton();
 
 private Singleton() {}
 
 public static Singleton getInstance() {
 return uniqueInstance;
 }
 }
 
 | 
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 
 | // code6public class Singleton {
 
 private static Singleton uniqueInstance;
 
 static {
 uniqueInstance = new Singleton();
 }
 
 private Singleton() {}
 
 public static Singleton getInstance() {
 return uniqueInstance;
 }
 }
 
 | 
在类加载的时候直接创建这个对象,这样既能提高效率,又能保证线程安全,code5和code6几乎没有区别,因为静态成员变量和静态代码块都是类初始化的时候被加载
第四种(静态内部类)
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 
 | // code7public class Singleton {
 
 private static class SingletonHolder {
 private static Singleton uniqueInstance = new Singleton();
 }
 
 private Singleton() {}
 
 public static Singleton getInstance() {
 return SingletonHolder.uniqueInstance;
 }
 }
 
 | 
饿汉式的方式只要 Singleton 类被装载了,那么 uniqueInstance 就会被实例化(没有达到 lazy loading 效果),而这种方式是 Singleton 类被装载了,uniqueInstance 不一定被初始化。因为 SingletonHolder 类没有被主动使用,只有显示通过调用 getInstance 方法时,才会显示装载 SingletonHolder 类,从而实例化 uniqueInstance
第五种(枚举)
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 
 | // code8public enum Singleton {
 
 INSTANCE;
 
 public void doSomething() {
 System.out.println("单例对象的一个方法");
 }
 
 public static void main(String[] args) {
 Singleton.INSTANCE.doSomething();
 }
 }
 
 | 
通过 Singleton.INSTANCE 来获取枚举对象。
用枚举实现单例模式可以避免如下 2 个问题,其他四种方式都不能避免
- 序列化造成单例模式不安全
- 反射造成单例模式不安全