本文中主要阐述一下懒汉模式实现中的各种问题
一、单例模式有两种实现方式:
- 饿汉模式
- 懒汉模式(延迟加载)
二、实现单例模式中比较重要的三个点是:
- 构造方法私有
- 定义静态成员变量
- 对外提供静态方法
三、第一个单例:
下面的单例中,构造方法私有,静态成员在变量在静态方法中提供出去了,并且在方法中加上了synchronized关键字,保证了线程安全
package com.scott;
/**
* 单例模式
*/
public class Singleton {
/** 使用 volatile 保证实例变量的原子性操作 */
private volatile static Singleton instance;
/** 私有化构造方法 */
private Singleton() { }
/**
* 获取实例
* @return 实例
*/
public static synchronized Singleton getSingletonInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
上面这个无疑是一个单例,并且是线程安全的。方法上面为什么加了synchroized:
如果不加synchroized锁会发生什么情况呢?下面来分析一下:
- 假如存在线程A和线程B同时调用了getSingletonInstance方法
- 当线程A执行了 if(instance == null) 后,结果为true 但是CPU时间片用完了,线程A将CPU执行权交出去了
- 此时线程B获取到了CPU的执行权,线程B执行。线程B判断 if(instance == null) 也为 true,因为线程A还没有创建Singletong对象。线程B继续执行完整个方法,所以线程B创建了一个Singleton对象。
- 此时线程A获取到了CPU的执行权,继续执行instance = new Singleton(),此时线程A也创建了一个Singleton对象
但是上面这个单例存在一个问题synchroized太重了,性能相对来说不够高。所以下面使用双重检查锁来实现单例模式
四、第二个单例(第一版双重检查锁)
/**
* 单例模式
*
* 使用 synchronized 保证在多线程下依旧是同一个实例。
* 双重检查加锁,减少加锁的性能消耗
*/
public class Singleton {
/** 静态私有变量 */
private static Singleton instance;
/** 私有化构造方法 */
private Singleton() { }
/**
* 获取实例
* @return 实例
*/
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
如上述代码中所示,这里面存在两个判空。通过多次判空来保证对象是否存在。为什么需要两个判空操作:
- 线程A和线程B同时来调用getInstance()方法
- 线程A获取到了锁,线程B还没获取到锁,但是线程B已经执行了第一个为空判断,线程B此时在等待锁
- 线程A执行完整个方法,获取到了Singleton对象,并且释放了锁
- 线程B获取到了锁,此时如果不再进行一次为空判断,则线程B会重复的创建一个Singleton对象。
五、volatile对象保证可见性和原子性
如上面第四点中的代码所示,虽然保证了单例,但是代码还是存在问题的。可能会出现获取到空对象的情况。
假如存在三条线程同时访问getInstance()方法:
- 线程A和线程B都已经执行到sychronized所在的行,此时一定是其中一条线程获取到锁对象,假设此时线程A获取到了锁
- 线程A获取到了锁,线程A继续执行了instance == null行,在执行instance == new Singleton()行时,发生了指令重排,
- 这里说明一下 new Singleton()做了什么事:
- 申请了内存空间
- 完成属性的初始化
- 将内存地址交给instance变量保存
- 这里说明一下 new Singleton()做了什么事:
- 线程A先将内存地址交给instance变量保存了,但是还没有完成属性的初始化。
- 此时在线程A完成new Singleton()的属性初始化之前线程A的CPU时间片用完了,
- 此时三个线程的状态分别是:线程C进来了,线程B在获取锁的地方被阻塞,线程A没有时间片
- 线程C执行第一个为空判断,发现对象已经存在了,直接返回使用了
为了防止这种情况下的异常,所以静态成员变量使用volatile修改,volatile可以保证变量的可见性和原子性操作
可见性使用MESI协议(内存一致性协议)保证了变量在多个CPU高速缓存区中的可见性
volatile保证了不会轻易的出现指令重排序。new Singleton() 最终执行时是通过多个字节码指令执行的
所以在上述第四点的代码中静态变量还需要使用volatile修饰。