单例模式笔记
单例模式是限制类的实例只有一个的设计模式。
代码
单线程下的单例模式代码
public class SimpleSingleton { private static SimpleSingleton simpleSingleton; private SimpleSingleton() {} public static SimpleSingleton getInstance() { // 如果 simpleSingleton 未进行实例化,则创建实例,之后都使用此实例 if (simpleSingleton == null) { simpleSingleton = new SimpleSingleton(); } return simpleSingleton; } }
但是如果在多线程的情况下,可能出现在第一个线程 A 线程判断simpleSingleton
为空时,进入simpleSingleton = new Simpleton();
部分进行代码实例化,但在 A 线程还未实例化结束时,另一个线程 B 线程进行判断simpleSingleton
是否为空,此时由于 A 线程还没有实例化赋值给simpleSingleton
,所以其仍未null
,这是 B 线程也会进入执行simpleSingleton = new Simpleton();
,这样就会导致出现不同的实例,所以这种方法只能用在单线程的情况下。
多线程下的单例模式代码
多线程下最简单的方式是对getInstance()
方法整个加上synchronized
修饰符,保证每次只有一个线程能进入此方法。但是这样做会很影响性能,我们应该尽量减小锁作用的范围,所以最好采用如下的双重加锁方式:
public class SynchronizedSingleton { private static SynchronizedSingleton singleton; private SynchronizedSingleton() {} public static SynchronizedSingleton getInstance() { if (singleton == null) { synchronized (SynchronizedSingleton.class) { if (singleton == null) { singleton = new SynchronizedSingleton(); } } } return singleton; } }
之所以进行第二次singleton == null
判断是因为在线程 A 在第一次进行判断为null
后获得锁进行实例化,在实例化未完成时,B 线程判断仍为null
,这是由于获得不了锁,所以等待,在 A 线程创建实例后释放了锁,这时 B 线程获得锁并执行,如果此时不进行第二次的singleton == null
判断,则 B 线程也会创建一个新的实例,导致单例模式出现问题;而如果进行第二次判断,则会得知singleton
已经被实例化,就不会再创建新的实例。
目前一切看起来都很美好,但是仍然有一个问题需要解决。
创建对象实例可以分为三个步骤:
-
分配内存
-
调用构造函数
-
将对象指向分配的内存地址
之前的代码如果依此顺序执行,则不会有问题。但是为了提高性能,编译器和处理器常常会对指令进行重排序,这时如果步骤 2 和步骤 3 的顺序颠倒了,先将对象指向分配的内存地址,后执行构造函数那么就会出现问题。当 A 线程将对象指向分配的内存地址,但还未执行构造函数的时候, B 线程进入,判断对象不为空,则将对象引用返回,这时如果使用此引用,则会出现问题。
目前有三个解决方法:
1. 给静态实例属性加上 volatile关键字(需要 JDK 1.5 及之后版本)
private static volatile SynchronizedSingleton singleton;
volatile 关键字可以保证对 volatile 变量的操作不会进行重排序。
2. 使用单个元素的枚举类型(需要 JDK 1.5 及之后版本)
public enum Singleton { INSTANCE; }
3. 使用子类,由 JVM 保证单例
public class InnerClassSingleton { public static Singleton getInstance() { return Singleton.singleton; } private static class Singleton { static Singleton singleton = new Singleton(); } }
类的静态属性只会在第一次加载的时候初始化一次,同时 JVM 保证在初始化的过程中(未完成时)无法被使用。