单例模式一个是设计模式中最常见的一种了,网上有n多中写法。顾名思义单例模式就是只有一个该类的实例。实现方式大概有一下几类:
- 饿汉式
- 懒汉式
- 枚举模式
首先最基本的肯定是将构造函数设为私有,在此基础上几种写法各有特点,主要是因为三个性能指标的影响:
- lazy loading
- 线程安全
- 序列化攻击
1、懒汉式[不可用]
1 | public class Singleton { |
- 优点:实现了Lazy Loading,第一次使用的时候才真正实例化
- 缺点:无法保证线程安全
2、饿汉式[可用]
1 | public class Singleton { |
- 优点:这种写法的优点就是线程安全,利用类加载机制避免了多线程问题
- 缺点:在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费。
3、懒汉式改进-双重检查(还是不安全)
针对懒汉式的线程不安全,可以通过双重加锁检查(DCL)的机制来避免:
1 | public class Singleton { |
将singleton声明volatile保证可见性,再通过加锁实现线程安全,最大限度的减少了加锁带来的性能损耗。
更新一下:
在小米面试的时候面试官指出:即使是双重检测,也还是无法保证线程安全。
后来上网找了一下,确实发现了这个以前没有注意到的细节。比如下面这种情况:
- 步骤1中线程1检测到singleton为null,于是进入步骤2获取锁,到步骤4中,开始实例化一个实例。
- 这里问题出来了,在实例化的过程中,执行构造方法之前,singleton就已经是非null的了
- 这个时候如果线程2执行到步骤1,检测到singleton为非null,那么就直接返回这个对象了,这个对象有可能还并没有执行构造函数,也就是说这里检测singleton是否为null,只能保证是否有这个实例了,但是却没法保证这个实例的构造过程是否已经完全执行完毕。
这里涉及到JVM的一个无序写入的问题,总之这个问题以前一直被忽略,直到面试官提出。所以自己闭门造车还是有局限性的,多和牛人交流,也是提升自己的一个办法。
再次更新:
在Java并发编程实战的最后一章倒数第三页上也介绍了这个问题,奈何当时面试的时候还没看到这个部分。
首先否定了这个方法的正确性:线程可能看到一个仅被部分构造的实例。
下面再引用一段书中的总结:
在JVM(书上写的是JMM,我怀疑是写错了)的后续版本中,如果将单例声明为volatile类型,你们就能启动DCL,并且这种方式对性能的影响很小。因为volatile变量读取操作的性能通常只是略高于非volatile变量。
然而,DCL这种使用方法已经被广泛地废弃了,因为促使该模式出现的原因(无竞争同步的执行速度很慢,以及JVM启动时很慢)已经不复存在,因而它不再是一种高效的优化措施。延迟初始化占位类模式能带来同样的优势,并且更容易理解。
书中说的延迟初始化占位类模式,也就是下面要介绍的静态内部类。
4、饿汉式改进-静态内部类
饿汉式主要缺点就是无法实现Lazy Loading,可以通过静态内部类来解决这一问题
1 | public class Singleton { |
与上面的直接定义final实例不同的是定义一个静态的内部类,在内部类中持有这个实例,这样只有在第一次获取实例的时候才会加载这个内部类,从而实现Lazy Loading。
5、枚举实现
方法3和方法4都实现了Lazy Loading也保证了线程安全,但单例攻击还会面临一个问题就是序列化。于是就有了枚举类型的单例:
1 | public class EnumSingleton { |