单例模式

动机

有时候一个类只有一个实例对象是很重要的。比如,在一个系统中只能有一个 window manager。通常单例模式被用于内部的或者外部的资源的中央管理,它们为自己提供一个全局的访问点。

单例模式是最简单的设计模式之一:它只涉及一个负责实例化自身的类,以确保这个类最多只实例化出一个对象。同时,它提供一个全局访问这个实例的方法。在这种情况下,可以在任何地方使用相同的实例,不可能每次都直接调用构造函数。

意图

实现

1. 懒加载 - 非线程安全

public class UnsafeLazySingleton {
    private static UnsafeLazySingleton instance;

    private UnsafeLazySingleton() {}

    public static UnsafeLazySingleton getInstance() {
        if(instance == null) {
            instance = new UnsafeLazySingleton();
        }
        return instance;
    }
}

适用场景:没有并发的环境

优点:能够避免内存不必要的浪费

缺点:在并发环境下,会造成一个类创建出多个实例的情况。

2. 懒加载 - 线程安全(synchronized 控制)

public class SafeLazySingleton {

    private static SafeLazySingleton instance;

    private SafeLazySingleton() {}

    public static synchronized SafeLazySingleton getInstance() {
        
        if(instance == null) {
            instance = new SafeLazySingleton();
        }

        return instance;
    }
}

显然,通过 synchronized 排斥锁解决了并发环境下可能造成 instance 多次初始化的情况。但是这样做,会导致程序执行性能下降。

为了提高程序的执行性能,我们可以通过双重检查锁定。

public class DoubleCheckSingleton {

    private static DoubleCheckSingleton instance;

    private DoubleCheckSingleton() {}

    public static DoubleCheckSingleton getInstance() {
        if(instance != null) {
            synchronized(DoubleCheckSingleton.class) {
                if(instance != null) {
                    instance = new DoubleCheckSingleton();
                }
            }
        }

        return instance;
    }
}

双重检查锁定的实现,提高了程序执行性能,但是它是非线程安全的。为什么这么说呢?

关键是这条语句:instance = new DoubleCheckSingleton();

这行代码可以分解为如下 3 行伪代码:

memory = allocate();  //1. 为新生对象分配内存空间
ctorInstance(memory); //2. 初始化对象
instance = memory;    //3. 设置 instance 指向刚分配的内存地址

上面 3 行伪代码中的 2 和 3 之间可能会被重排序。当 2 和 3 发生重排序的时候,伪代码如下:

memory = allocate();  //1. 为新生对象分配内存空间
instance = memory;    //3. 设置 instance 指向刚分配的内存地址
ctorInstance(memory); //2. 初始化对象

那么,我们来模拟一下在并发环境下可能出现的情况:

从上图可知,在线程 A 成功获取到 DoubleCheckSingleton 的排斥锁之后,线程 A 开始执行实例过程。线程 A 成功执行完 设置 instance 指向内存空间 这步骤之后,CPU 分配时间给线程 B 去调用 DoubleCheckSingleton.getInstance() 方法。这个时候的 instance 自然不为空,但是却没有初始化。到此为止,已经解释了上面这种双重检查锁定的非线程安全的原因。

知晓问题发生的原因之后,我们可以有 2 个办法来实现双重检查锁定在并发环境下的线程安全。

  1. 禁止步骤 2 和 3 的重排序。

  2. 允许 2 和 3 的重排序,但不允许其他线程 “看到” 这个重排序。

2.1 基于 volatile 的解决方案

有兴趣可以看看这篇文章,讲述了 volatile 的内存语义 -> volatile、synchronized 和 final 的内存语义

实现代码如下:

public class DoubleCheckSingleton {

    private volatile static DoubleCheckSingleton instance;

    private DoubleCheckSingleton() {}

    public static DoubleCheckSingleton getInstance() {
        if(instance != null) {
            synchronized(DoubleCheckSingleton.class) {
                if(instance != null) {
                    instance = new DoubleCheckSingleton();
                }
            }
        }
        return instance;
    }
}

2.2 基于类初始话的解决方案

JVM 在类的初始化阶段(即在 Class 被加载后,且被线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM 会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化(简单点理解就是避免类被多次初始化)。

基于这个特性,实现代码如下:

public class InstanceFactory {
    private static class InstanceHolder {
        public static Singleton instance = new Singleton();
    }

    public static Singleton getInstance() {
        return InstanceHolder.instance;
    }
}

3. 饿汉模式(线程安全)

public class EarlySingleton {

    private static EarlySingleton instance = new EarlySingleton();

    private EarlySingleton() {}

    public static EarlySingleton getInstance() {
        return instance;
    }
}

优点:线程安全

缺点:会造成内存的浪费

4. 如何避免反序列化对单例模式的破坏

当一个类实现了序列化的时候,我们来看一下反序列出来的对象是否还是同一个对象。

public class SingletonSerialized implements Serializable {

	private static final long serialVersionUID = 1L;

	private static SingletonSerialized instance = new SingletonSerialized();
	
	private SingletonSerialized() {
		
		System.out.println("SingletonSerialized is creating");
	}
	
	public static SingletonSerialized getInstance() {
		return instance;
	}
}

测试代码

public class SingletonSerializedTest implements Serializable {

	@Test
	public void testSingletonSerialized() throws IOException, ClassNotFoundException {
		SingletonSerialized serializedObj;
		SingletonSerialized instance = SingletonSerialized.getInstance();
		
		FileOutputStream fos = new FileOutputStream("serializedObj.obj");
		ObjectOutputStream oos = new ObjectOutputStream(fos);
		oos.writeObject(instance);  
        oos.flush();  
        oos.close(); 
        
        FileInputStream fis = new FileInputStream("serializedObj.obj");  
        ObjectInputStream ois = new ObjectInputStream(fis); 
        serializedObj = (SingletonSerialized) ois.readObject();
        assertEquals(instance, serializedObj);
	}
}

结果:测试未通过

那么,有什么方法可以解决反序列化时对象重新创建?

来看下,java.io.Serializable 中的注释,如下:

ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;

This writeReplace method is invoked by serialization if the method exists and it would be accessible from a method defined within the class of the object being serialized. Thus, the method can have private, protected and package-private access. Subclass access to this method follows java accessibility rules.

ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;

This readResolve method follows the same invocation rules and accessibility rules as writeReplace.

从上面的注释可以看出,如果实现了序列化接口的类同时也实现了 writeReplace() 或 readResolve() 这两个方法中的一个,那么在序列化和反序列化的时候就会调用 writeReplace() 和 readResolve() 这两个方法。

了解过 readResolve() 方法之后,我们稍微改一下 SingletonSerialized 类的构造。

public class SingletonSerialized implements Serializable {

	private static final long serialVersionUID = 1L;

	private static SingletonSerialized instance = new SingletonSerialized();
	
	private SingletonSerialized() {
		
		System.out.println("SingletonSerialized is creating");
	}
	
	public static SingletonSerialized getInstance() {
		return instance;
	}
	
	private Object readResolve() {
		return instance;
	}
}

最后,重新执行 SingletonSerializedTest 中的测试方法,结果 work as expected。

BACK