如何建立完美的单例模式?

原文做者: Ankit Sinhalhtml

原文地址:How to make the perfect Singleton?java

译者: sunluyaoandroid

设计模式在软件开发者中十分受欢迎。设计模式是对于常见软件问题的良好解决方案。单例模式是 Java 中建立型设计模式的一种。数据库

单例模式的目的是什么?

单例类的目的是控制对象建立,约束对象的数量有且只有一个。单例模式只容许有一个入口来建立类实例。设计模式

由于只有一个单例类实例,任何单例类的实例都将只会产生一个类,就像静态域同样。当你须要控制资源的时候,例如在数据库链接或者使用 sockets ,单例模式是很是有用的。api

这看起来是一个很简单的设计模式,可是当咱们真正去实现的时候,会带来许多的实现问题。单例模式的实如今开发者当中老是存在必定争议。如今,咱们将会讨论一下如何建立一个单例类以完成下列目的:安全

限制类的实例而且保证在 JVM 中只存在一个类实例。微信

让咱们在 Java 中建立单例类并在不一样的状况下进行测试。多线程

建立单例类

为了实现单例类,最简单方法是把构造器变为 private。有两种初始化方法。oracle

饿汉式

饿汉式初始化,单例类的实例在类加载时被建立,这是建立单例类最简单的方法。

经过将构造器声明为 private ,不容许其余类来建立单例类实例。取而代之的是,建立一个静态方法(一般命名为 getInstance)来提供建立类实例的惟一入口。

public class SingletonClass {

	private static volatile SingletonClass sSoleInstance = new SingletonClass();

	//private constructor.
	private SingletonClass(){}

	public static SingletonClass getInstance() {
    	return sSoleInstance;
	}
}
复制代码

这种方法有一个缺陷,即便在程序没有使用到它的时候,实例已经被建立了。当你建立数据库链接或者 socket 时,这可能成为一个至关大的问题,会致使内存泄漏问题。解决方法是当须要的时候再建立实例,咱们称之为懒汉式初始化。

懒汉式

与饿汉式相反,你在 getInstance() 方法中初始化类实例。方法中将会判断类实例是否已经建立,若是已经存在,将返回旧的实例,反之在 JVM 中建立新的实例并返回。

public class SingletonClass {

	private static SingletonClass sSoleInstance;

	private SingletonClass(){}  //private constructor.

	public static SingletonClass getInstance(){
    	if (sSoleInstance == null){ //if there is no instance available... create new one
        sSoleInstance = new SingletonClass();
    	}

    return sSoleInstance;
	}
}
复制代码

咱们都知道在 Java 中,若是两个对象是相同的,那么他们的 hashCode 也是相同的。让咱们测试一下,若是上面的单例类都正确实现,那么将会返回一样的哈希。

public class SingletonTester {
	public static void main(String[] args) {
    	//Instance 1
    	SingletonClass instance1 = SingletonClass.getInstance();

    	//Instance 2
    	SingletonClass instance2 = SingletonClass.getInstance();

    	//now lets check the hash key.
    	System.out.println("Instance 1 hash:" + instance1.hashCode());
    	System.out.println("Instance 2 hash:" + instance2.hashCode());  
	}
}
复制代码

下面是输出日志:

15:04:341 I/System.out: Instance 1 hash:247127865
15:04:342 I/System.out: Instance 2 hash:247127865
复制代码

能够看到两个实例拥有一样的 hashCode。因此,这就意味着上面的代码建立了完美的单例类,是吗?不。

让单例类反射安全

在上面的单例类中,经过反射能够建立不止一个实例。 Java Reflection 是一个在运行时检测或者修改类的运行时行为的过程。经过在运行时修改构造器的可见性并经过构造器建立实例能够产生新的单例类实例。运行下面的代码,单例类还存在吗?

public class SingletonTester {
	public static void main(String[] args) {
    	//Create the 1st instance
    	SingletonClass instance1 = SingletonClass.getInstance();
    
    	//Create 2nd instance using Java Reflection API.
    	SingletonClass instance2 = null;
    	try {
        	Class<SingletonClass> clazz = SingletonClass.class;
        	Constructor<SingletonClass> cons = clazz.getDeclaredConstructor();
        	cons.setAccessible(true);
        	instance2 = cons.newInstance();
    	} catch (NoSuchMethodException | 	InvocationTargetException | 	IllegalAccessException | 	InstantiationException e) {
        	e.printStackTrace();
    	}

    	//now lets check the hash key.
    	System.out.println("Instance 1 hash:" + instance1.hashCode());
    	System.out.println("Instance 2 hash:" + instance2.hashCode());
	}
}
复制代码

下面是输出日志:

15:21:48.216 I/System.out: Instance 1 hash:51110277
15:21:48.216 I/System.out: Instance 2 hash:212057050
复制代码

每个实例都有不一样的 hashCode。显然这个单例类没法经过测试。

解决方案:

为了预防反射致使的单例失败,当构造器已经初始化而且其余类再次初始化时,抛出一个运行时异常。让咱们更新 SingletonClass.java

public class SingletonClass {

	private static SingletonClass sSoleInstance;

	//private constructor.
	private SingletonClass(){
   
    	//Prevent form the reflection api.
    	if (sSoleInstance != null){
        	throw new RuntimeException("Use getInstance() method to get the single instance of this class.");
    	}
	} 

	public static SingletonClass getInstance(){
    	if (sSoleInstance == null){ //if there is no instance available... create new one
        	sSoleInstance = new SingletonClass();
    	}

    	return sSoleInstance;
	}
}
复制代码

让单例类线程安全

若是两个线程几乎同时尝试初始化单例类,将会发生什么?让咱们测试下面的代码,两个线程几乎同时被建立而且调用 getInstance()

public class SingletonTester {
	public static void main(String[] args) {
    	//Thread 1
    	Thread t1 = new Thread(new Runnable() {
        	@Override
        	public void run() {
            	SingletonClass instance1 = SingletonClass.getInstance();
            	System.out.println("Instance 1 hash:" + instance1.hashCode());
        	}
    	});

    	//Thread 2
    	Thread t2 = new Thread(new Runnable() {
        	@Override
        	public void run() {
            	SingletonClass instance2 = SingletonClass.getInstance();
            	System.out.println("Instance 2 hash:" + instance2.hashCode());
        	}
    	});

    	//start both the threads
    	t1.start();
    	t2.start();
	}
}	
复制代码

若是你屡次运行这些代码,有时你会发现不一样的线程建立了不一样的实例。

16:16:24.148 I/System.out: Instance 1 hash:247127865
16:16:24.148 I/System.out: Instance 2 hash:267260104
复制代码

这说明了你的单例类不是线程安全的。全部的线程同时调用 getInstance()方法,sSoleInstance == null 条件对全部线程返回值,因此两个不一样的实例被建立。这打破了单例原则。

解决方案

同步 getInstance() 方法

public class SingletonClass {

	private static SingletonClass sSoleInstance;

	//private constructor.
	private SingletonClass(){
   
    	//Prevent form the reflection api.
    	if (sSoleInstance != null){
        	throw new RuntimeException("Use getInstance() method to get the single instance of this class.");
    	}
	} 

	public synchronized static SingletonClass getInstance(){
    	if (sSoleInstance == null){ //if there is no instance available... create new one
        	sSoleInstance = new SingletonClass();
    	}

    	return sSoleInstance;
	}
}
复制代码

在咱们同步 getInstance() 方法以后,第二个线程必须等到第一个线程执行完 getInstance() 方法以后才能执行,这就保证了线程安全。

可是,这个方法一样有一些缺点:

  • 锁的开销致使运行变慢
  • 实例变量初始化以后的同步操做时没必要要的

双检查锁

使用 双检查锁 方法建立实例能够克服上面的问题。

这这种方法中,当实例为空时,在同步代码块中建立单例类,这样只有当 sSoleInstance 为空时,同步代码块才会执行,避免了没必要要的同步操做。

public class SingletonClass {

	private static SingletonClass sSoleInstance;

	//private constructor.
	private SingletonClass(){

    	//Prevent form the reflection api.
    	if (sSoleInstance != null){
        	throw new RuntimeException("Use getInstance() method to get the single instance of this class.");
    	}
	}

	public static SingletonClass getInstance() {
    	//Double check locking pattern
    	if (sSoleInstance == null) { //Check for the first time
      
        	synchronized (SingletonClass.class) {   //Check for the second time.
          	//if there is no instance available... create new one
          	if (sSoleInstance == null) sSoleInstance = new SingletonClass();
        	}
    	}

    	return sSoleInstance;
	}
}
复制代码

使用 volatile 关键字

表面上看,这个方法看起来很完美,你只须要付出一次静态代码块的代价。可是除非你使用 volatile 关键字,不然单例仍然会被打破。

没有 volatile 修饰符,另外一个线程可能在变量 sSoleInstance 正在初始化还没有完成时引用它。可是经过 volatile 的保证 happens-before 关系,全部对于 sSoleInstance 变量的写操做都会在读操做以前发生。

public class SingletonClass {

	private static volatile SingletonClass sSoleInstance;

	//private constructor.
	private SingletonClass(){

    	//Prevent form the reflection api.
    	if (sSoleInstance != null){
        	throw new RuntimeException("Use getInstance() method to get the single instance of this class.");
    	}
	}

	public static SingletonClass getInstance() {
    	//Double check locking pattern
    	if (sSoleInstance == null) { //Check for the first time
      
        	synchronized (SingletonClass.class) {   //Check for the second time.
          	//if there is no instance available... create new one
          	if (sSoleInstance == null) sSoleInstance = new SingletonClass();
        	}
    	}

    	return sSoleInstance;
	}
}
复制代码

如今上面的单例类是线程安全的。在多线程应用环境中(好比安卓应用)保证单例类的线程安全是必需的。

让单例类序列化安全

在分布式系统中,有些状况下你须要在单例类中实现 Serializable 接口。这样你能够在文件系统中存储它的状态而且在稍后的某一时间点取出。

让咱们测试一个这个单例类在序列化和反序列化以后是否仍然保持单例。

public class SingletonTester {
	public static void main(String[] args) {
  
  		try {
    	    SingletonClass instance1 = SingletonClass.getInstance();
    	    ObjectOutput out = null;

    	    out = new ObjectOutputStream(new FileOutputStream("filename.ser"));
    	    out.writeObject(instance1);
    	    out.close();

    	    //deserialize from file to object
    	    ObjectInput in = new ObjectInputStream(new FileInputStream("filename.ser"));
    	    SingletonClass instance2 = (SingletonClass) in.readObject();
    	    in.close();

    	    System.out.println("instance1 hashCode=" + instance1.hashCode());
    	    System.out.println("instance2 hashCode=" + instance2.hashCode());

    	} catch (IOException | ClassNotFoundException e) {
    	    e.printStackTrace();
    	}
  }
}


16:16:24.148 I/System.out: Instance 1 hash:247127865
16:16:24.148 I/System.out: Instance 2 hash:267260104
复制代码

能够看到实例的 hashCode 是不一样的,违反了单例原则。序列化单例类以后,当咱们反序列化时,会建立一个新的类实例。为了预防另外一个实例的产生,你须要提供 readResolve() 方法的实现。readResolve()代替了从流中读取对象。这就确保了在序列化和反序列化的过程当中没人能够建立新的实例。

public class SingletonClass implements Serializable {

	private static volatile SingletonClass sSoleInstance;

	//private constructor.
	private SingletonClass(){

    	//Prevent form the reflection api.
    	if (sSoleInstance != null){
    	    throw new RuntimeException("Use getInstance() method to get the single instance of this class.");
   	 }
	}

	public static SingletonClass getInstance() {
    	if (sSoleInstance == null) { //if there is no instance available... create new one
    	    synchronized (SingletonClass.class) {
    	        if (sSoleInstance == null) 	sSoleInstance = new SingletonClass();
    	    }
   	 }

    	return sSoleInstance;
	}

	//Make singleton from serialize and deserialize operation.
	protected SingletonClass readResolve() {
    	return getInstance();
	}
}
复制代码

结论

在文章的最后,你能够建立线程,反射和序列化安全的单例类,但这仍然不是完美的单例,你可使用克隆或者多个类加载器来建立不止一个实例。可是对于大多数应用,上面的实现方法已经能够很好的工做了。

文章同步更新于微信公众号: 秉心说 , 专一 Java 、 Android 原创知识分享,LeetCode 题解,欢迎关注!

相关文章
相关标签/搜索