TOC
本文将通过梳理编码的思考过程,来讲述各种单例模式实现的缘由和注意事项;
单例模式
单例模式各种实现方式中,最容易想到方式就是饿汉式单例,代码实现如下:
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
如果单例对象,不能通过简单的 new 生成,则可以通过静态代码块来实现
public class Singleton {
private static Singleton instance = null;
static {
instance = new Singleton();
... //other initial operation
}
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
饿汉式的代码实现十分简单,就不需要多解释了;饿汉式的实现最明显的缺点就是无法做到懒加载;懒加载最经典的方式就是双重检查锁方式的实现;
下面我们看看双重检查锁方式的实现思路,先实现一个最简版本的懒加载方式的单例模式:
public class Singleton {
private static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if(null == instance) {
instance = new Sigleton();
}
return instance;
}
}
上述实现方式,确实做到了懒加载,但却有并发问题,如果多线程调用 getIntance() 方法,则有可能出现多个线程进入 null 判断的代码块中,最终导致 JVM 内有多个单例实例对象;
对于多线程调用导致的并发问题,我们第一个想到的解决方案,就是 synchronized 关键字了,下面我们看下 sychronized 方式的实现;
public class Singleton {
private static Singleton instance = null;
private Singleton() {}
public synchronized static Singleton getInstance() {
if(null == instance) {
instance = new Sigleton();
}
return instance;
}
}
实现十分简单,就是在 getIntance 方法上加上 synchrnoized 关键字;这样确实解决了多线程的问题,却引入了并发性能的问题;由于 getInstance 是静态方法,此时的 synchronized 加的是类级别的锁;那么要如何解决并发性能问题呢?那就是缩小 synchronized 的调用范围;
下面我们修改一下代码如下:
public class Singleton {
private static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if(null == instance) {
synchronized(Singleton.class) {
instance = new Singleton();
}
}
return instance;
}
}
上述实现解决了并发性能问题,却由于缩小了 synchronized 的加锁范围,又导致了并发问题;多线程运行情况下,仍然会出现多个线程同时进入 null 判断代码块内,最终 JVM 出现了两个实例对象;解决这个问题,方式也很简单,代码如下:
public class Singleton {
private static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if(null == instance) {
synchronized(Singleton.class) {
if(null == instance) {
instance = new Singleton();
}
}
}
return instance;
}
}
到这里,我们的实现方式跟双重检查锁的方式很接近了,并且也能够意识到实现中的两次 if 判断的作用:
- 第一个 if 的目的是缩小锁的范围,避免并发性能问题;
- 第二个 if 的目的是保证线程安全,解决多个线程进入初始化代码块重复初始化对象;
细心的读者可能发现了,上述实现方式跟双重检查锁的方式还有出入,那就是 instance 对象没有加上 volatile 关键字;下面我们看看标准的双重检查锁方式实现:
public class Singleton {
private static volatile Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if(null == instance) {
synchronized(Singleton.class) {
if(null == instance) {
instance = new Singleton();
}
}
}
return instance;
}
}
至此,我们就实现了双重检查锁的单例模式了,但双重检测锁的方式为什么必须加上 volatile ?
我们知道 volatile 关键字主要有两个作用,保证可见性和防止指令重排序;而在双重检测锁中,我们使用了 synchronizaed 关键字,所以可见性已经保证了;因此,关键就在于 volatile 另一个功能防止指令重排序;
原因是这样的,在 Java 中初始化对象本身并不是一个原子操作,初始化对象至少分为了以下三个步骤
- 申请 JVM 堆内存空间;
- 初始化对象,调用对象的构造方法;
- 将申请的堆内存空间的地址赋值给对象;(此时对象不是 null)
而 CPU 的指令重排序的存在,实际执行过程有可能是 1 -> 3 -> 2 ;假设「线程 1」 初始化对象,执行了 3 但还未执行 2;此时「线程 2」 会发现对象不是 null,认为对象初始化完毕,立即使用此对象,而对象实际还未初始化完毕,「线程 2」 将立即出现异常;volatile 关键字则保证了初始化对象的始终按照 1 -> 2 -> 3 的顺序,也就避免了对象未初始化完毕被使用的情况;
双重检测锁的方式已经基本没有问题了,但是实现上还是比较复杂,项目实现过程中难免会有一些小遗漏,导致不可知的系统错误;下面我们看看更加简洁的实现方式 - 静态内部类实现
public class Singleton {
private Singleton() {}
private static class Holder {
private static final Singleton singleton = new Sigleton();
}
public static Singleton getInstance() {
return Holder.sigleton;
}
}
静态内部类实现方式是利用了 JVM 类加载机制,而 JVM 加载静态内部类是通过懒加载的模式,并且保证了线程安全;因此,静态内部类的方式也就既能懒加载,又避免了并发问题;
到这里,我们实现的单例模式,具有懒加载,线程安全,性能高,实现简单等优点了;但我们看看下面的代码,仍然能够在 JVM 中创建不同的单例实例对象;
public class Sample {
public static void main(String[] args) {
Singleton singletonInstance = Singleton.getInstance();
Singleton reflectionInstance = null;
try {
Constructor[] constructors = Singleton.class.getDeclaredConstructors();
for (Constructor constructor : constructors) {
constructor.setAccessible(true);
reflectionInstance = (Singleton) constructor.newInstance();
}
} catch (Exception ex) {
throw new RuntimeException(ex);
}
System.out.println("singletonInstance hashCode: " + singletonInstance.hashCode());
System.out.println("reflectionInstance hashCode: " + reflectionInstance.hashCode());
}
}
运行结果
singletonInstance hashCode: 1618212626
reflectionInstance hashCode: 947679291
没错,就是通过反射,我们可以强行调用单例方法的私有构造函数,初始化出另一个不同的单例实例对象;那么如何避免这种情况了,关键就在与构造方法了
public class Singleton {
private volatile static Singleton singleton;
private Singleton() {
// protect against instantiation via reflection
if(singleton != null) {
throw new IllegalStateException("Singleton already initialized");
}
}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if(singleton == null)
singleton = new Singleton();
}
}
return singleton;
}
}
解决了反射问题,下面还有没有什么方式能够创建多个单例对象呢?这么问,当然是还有,那就是序列化与反序列化;下面我们看看如何通过序列化和反序列化初始化多个单例对象;
import java.io.*;
class Singleton implements Serializable {
private static final long serialVersionUID = 8806820726158932906L;
private volatile static Singleton singleton;
private Singleton() {
// protect against instantiation via reflection
if(singleton != null) {
throw new IllegalStateException("Singleton already initialized");
}
}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if(singleton == null)
singleton = new Singleton();
}
}
return singleton;
}
}
public class SingletonAndSerialization throws Exception{
public static void main(String[] args) {
Singleton instance1 = Singleton.getInstance();
// Serialize singleton object to a file.
ObjectOutput out = new ObjectOutputStream(new FileOutputStream("singleton.ser"));
out.writeObject(instance1);
out.close();
// Deserialize singleton object from the file
ObjectInput in = new ObjectInputStream(new FileInputStream("singleton.ser"));
Singleton instance2 = (Singleton) in.readObject();
in.close();
System.out.println("instance1 hashCode: " + instance1.hashCode());
System.out.println("instance2 hashCode: " + instance2.hashCode());
}
}
运行结果
# Output
instance1 hashCode: 1348949648
instance2 hashCode: 434091818
从上面代码运行结果我们可以知道,单例对象经过序列化,再反序列化便生成了一个不同的实例对象了。那么要如何解决这个问题呢?关键在于控制单例对象反序列化的行为,可以通过重写 Serializable 接口中的 readResolve() 方法来控制反序列化过程;
下面我们实现一个完整版的单例模式:
class SerializableSingleton implements Serializable {
private static final long serialVersionUID = 8806820726158932906L;
private static volatile SerializableSingleton instance;
private Singleton() {
// protect against instantiation via reflection
if(singleton != null) {
throw new IllegalStateException("Singleton already initialized");
}
}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if(singleton == null)
singleton = new Singleton();
}
}
return singleton;
}
// implement readResolve method to return the existing instance
protected Object readResolve() {
return instance;
}
}
这样就实现了健壮,高效的单例模式了,从中可以发现一个单例模式可以包含这么多的技术点,这也是为什么面试官总喜欢问单例模式的原因;但我们最终实现方式仍然十分复杂,最后的最后,我们就拿出大名鼎鼎的枚举类方式实现单例模式,这也是 Think in Java 中推荐的单例实现模式:
public enum Singleton {
INSTANCE;
public void anyMethod() {
}
}
# 使用方式
public class Client {
public static void main(String[] args) {
Singleton singleton = Singleton.INSTANCE;
singleton.anyMethod();
}
}
枚举类的单例模式就是这么简单,并且天然的解决了反射和序列化问题;
总结回顾
首先,我们使用饿汉式的方式实现单例模式;然后,解释了双重检查锁方式中两个 if 判断的作用,以及为什么要加 volatile 关键字;接着,引出了反射和反序列化导致的单例不安全,并且提供了相应的解决方案;最后,引出了枚举类的实现方式;