并发编程之单例模式
线程安全的单例模式一般认为有三种实现方式: 懒汉模式,枚举方式,静态内部类方式; 下面逐个来看下他们的实现方式和实现原理。
(1) 懒汉模式:
public class Singleton { private static volatile Singleton instance; private Singleton(){} /** * 双重检查锁实现可以有效提高效率 * 因为在大多数时候多处访问getInstance 方法时 是不需要创建实例的 * 所以外层的null 判断可以大大的减少排队等待时间 * 而里层的null 判断是用来在实例创建之初给第一次访问者用的 */ public static Singleton getInstance(){ if(null == instance){ // 语句③ synchronized (Singleton.class){ // 语句② if(null == instance){ // 语句① instance = new Singleton(); // 语句④ } } } return instance; } }
对于线程安全的考虑,主要是在获取单例模式的实例时调用getInstance方法会存在线程安全问题;只所以线程安全的懒汉模式需要如上实现,需要我们做如下考虑:
(1) 判语句①中判断 null == instance 时 可能有多个线程同时在判断,那么这些线程如果读取到的instance 都是null , 那么在第一个线程对instance 赋值之后,后续几个线程也会执行 ”instance = new Singleton()” 语句,单例模式将变成多例模式
(2) 对于(1) 的情况,我们可以考虑对Singleton 类加锁(对于语句②),也即在判断 instance 是否为null 之前做一个同步处理,这样可以保证不会有多个线程同时对 instance 做空判断,但是这样在高并发下会严重影响getInstance 方法的调用性能
(3) 鉴于(2) 中加锁操作的性能问题, 对于那些不是首次调用 getInstance 方法的语句在获取锁之前可以先判断一下 instance 实例是否已经创建好,即先做一次 null == instance (语句③) 的判断,只有当发现instance 为null 时才继续去创建instance
(4) 以上三个步骤的实现,叫做"双重检查锁" 的做法,这样做看起来似乎已经线程安全且也算高效了,但是对于语句④,我们需要注意的是这个语句不是原子操作,在语句④执行时,可能会出现语句④执行到一半,另外某个线程执行了语句③
这个时候语句③的判断结果可能会是 null != instance , 但是返回的 instance 也不是一个真正可用的实例,这样将导致实例刚创建时偶尔会出现 getInstance得到的实例无法正常使用。
具体原因如下:
instance = new Singleton(); 语句可以拆分为以下三个步骤
操作①: objRef = allocate(Singleton.class)
操作②: invokeConstructor(Singleton.class)
操作③: instance = objRef
由于编译器在编译指令时会对语句进行便于CPU流水化执行重排序,因而常会将 操作③ 的执行顺序放到操作②之前,因此执行顺序就编程了 ①③②; 这样如果在操作③执行完之后其它线程就开始执行判断上述代码段的
语句③ 时将会直接把 未调用语句②的instance 直接返回,拿到的将是一个未调用构造方法初始化的空实例。
这个的解决办法也很简单,只需要在Singleton 属性的定义时使用 volatile 关键字修饰,就能有效的防止 操作③被重排序,由于操作③是一个原子操作,并且操作前 instance == null ,操作后 instance 指向一个初始化
的Singleton实例,因此可以保证在语句③判断时,只要判断 instance !=null , 那么getInstance() 获取到的实例一定是初始化后的实例。
以上懒汉线程安全的单例模式近来有人不提倡使用,具体原因可参考原因;
(2) 静态内部类:
public class Singleton{ private Singleton(){} public static Singleton getInstance(){ return SingletonHolder.INSTANCE; } /** * 静态内部类 */ private static class SingletonHolder{ /** 静态内部类的静态属性 */ final static Singleton INSTANCE = new Singleton(); } }
使用静态内部类方式实现单例模式,主要基于JVM会在一个类的静态方法或者属性被调用时初始化该类的静态属性。因此当多个线程并发调用,第一个线程执行到getInstance() 方法的SingletonHolder.INSTANCE 时,
静态内部类 SingletonHolder 会被jvm 初始化,初始化完成之后调用才能继续; 这样就能确保包括第一次在内的所有调用都能拿到初始化只有的唯一实例。
(3) 枚举方式:
如果把单例模式定义成一个枚举,那么线程安全的实现方式可以更加简单,如下:
public enum Singleton { INSTANCE; Singleton(){} public void someService(){ } }
在调用时只需要直接使用 Singleton.INSTANCE.someService(); 就可以了, 这里其实和静态内部类实现原理是一样的,INSTANCE 实例本身就是 final static 的Singleton 属性,当INSTANCE被调用,同样的触发了初始化,因此可以在所有访问之前生产单例模式的实例。