DP #1 Singleton Pattern线程安全问题
单例模式确保一个类只有一个实例,自行提供这个实例并向整个系统提供这个实例。
其中涉及到最主要的问题就是在多线程并发时线程安全问题。
单例模式的实现也有一个循序渐进的过程:
1.最基本要求:每次从getInstance()都能返回一个且唯一的一个Singleton对象。
2.稍微高一点的要求:能适应多线程并发访问。
3.再提高一点的要求:提高获得Singleton实例的性能。
4.最后一点要求是:实现懒加载(Lazy Load),在需要的时候才被构造。
——————————————————————————————–
1. 单例模式最基本的实现 = 私有构造函数 + 私有静态属性 + 公有静态Getter方法判断实例化
1 public class Singleton { 2 3 private static Singleton uniqueInstance = null; 4 5 private Singleton() {} 6 7 public static Singleton getInstance() { 8 if (uniqueInstance == null) { 9 uniqueInstance = new Singleton(); 10 } 11 return uniqueInstance; 12 } 13 }
——————————————————————————————–
2. 如果考虑线程并发,最直接的想法就是加线程锁(synchronized)来解决线程同步问题,可以适应多线程并发,但是这里有个很大的性能问题,每一次调用getInstance都会进行同步准备,代价很大,其实我们只有在创建实例的问题上才需要解决线程安全问题。
public class Singleton { private static Singleton uniqueInstance = null; private Singleton() {} public synchronized static Singleton getInstance() { if (uniqueInstance == null) { uniqueInstance = new Singleton(); } return uniqueInstance; } }
——————————————————————————————–
3. 为了提高性能,我们只希望在第一次创建Instance实例的时候进行同步,使用双重检测加锁(DCL)机制,判断是第一次实例化后锁定对象,然后再synchronized块中再次判断。
public class Singleton { private static Singleton uniqueInstance = null; private Singleton() {} public static Singleton getInstance() { if (uniqueInstance == null) { synchronized(Singleton.class) { if (uniqueInstance == null) { uniqueInstance = new Singleton(); } } } return uniqueInstance; } }
DCL的写法来实现单例是很多资料上推荐的写法,实际上是不完全正确的。代码看似没有问题,但是测试中却发现线程大量并发访问时会报NullPointException,调试很久没有结果。
主要原因是因为 uniqueInstance = new Singleton();这句代码并非原子操作,在JVM执行的汇编代码中分为以下几步:
步骤1:给Singleton的实例分配内存空间。
步骤2:初始化Singleton构造器
步骤3:将uniqueInstance指向第一步中分配的内存空间。
步骤2和步骤3在JMM中是无法保证顺序的,所以如果1-3-2的情况发生,将造成RuntimeException:
线程A进入同步块,执行步骤1-3,此时uniqueInstance已经指向了内存空间,但是Singleton还未构造,此时uniqueInstance已经非空,线程B直接return uniqueInstance。
关于添加volatile关键字,jvm虚拟机保证每次uniqueInstance的值都是从主内存中读取,这种方案是可行方案之一。
——————————————————————————————–
4. 重新设计后,我们把初始化实例的事情扔给JVM。因为JVM保证一个类在一个ClassLoader中只会被初始化一次。
public class Singleton { private static Singleton uniqueInstance = new Singleton(); private Singleton() {} public static Singleton getInstance() { return uniqueInstance; } }
这是传说中的【饿汉式单例】,全局的单例在类装载时第一时间被构建并初始化。
优点:速度快,提前创建。
缺点:某些需要配置参数传入getInstance构建的Singleton无法使用
——————————————————————————————–
5. 我们还是回归第一次被使用时创建的懒加载,同样把初始化实例的事情交给JVM,这次我们使用内部私有类来创建实例。
public class Singleton { public static Singleton getInstance() { return SingletonHolder.INSTANCE; } private static class SingletonHolder { static final Singleton INSTANCE = new Singleton(); } }
这是【懒汉式单例】的一种,SingletonHolder是私有内部类,所以只有getInstance可以访问,读取实例的时候也不会同步,性能上和安全上比较平衡。
——————————————————————————————–
结语:
单例模式的实现方式还有很多,在不同语言不同需求下的实现也有不同的考量和标准,作为一个最简单又最复杂的设计模式,我们应该去尝试每种实现的潜在问题和解决方案,需要更多的理解和尝试。

浙公网安备 33010602011771号