联系我

单例模式面试连环炮

2020.08.30

面试官:请说一下什么是单例模式

小明:保证整个系统中一个类只有一个实例 。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。主要解决一个全局使用的类频繁地创建与销毁的问题。

面试官:请写一个单例模式

小明:(写了一个最简单的懒汉式单例模式,可以让面试官继续发问)

class Singleton{  
	private static Singleton instance = null;
    
	private Singleton(){}  
	  
	public static Singleton getInstance(){  
		if(instance == null){   
			instance = new Singleton();  
		}  
		return instance;  
	}  
}

面试官:这种实现方式线程安全吗?

小明:线程不安全,原因是 getInstance () 并不是一个原子方法,在高并发的环境中,会出现多个不同的实例的。

面试官:如何改进?

改进的方式很简单,加上同步就可以了。

class Singleton{  
	private static Singleton instance = null;
    
	private Singleton(){}  
	  
	public synchronized static Singleton getInstance(){  
		if(instance == null){   
			instance = new Singleton();  
		}  
		return instance;  
	}  
}

面试官:你这个可能有一个问题,你知道是什么吗?

虽然解决了线程安全问题,但是因为加上了 synchronized 关键字,在高并发环境下,多个线程相互竞争锁,导致效率大不如前,这里可以使用 Double-Check 提高效率:

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

面试官:这个代码在高并发下还是可能会生成多个实例?

对,因为 new Singleton () 并不是一个原子操作,大约可以分为 3 个步骤:

  1. 开堆内存开辟空间,分配地址
  2. 初始化该对象
  3. 将内存地址传递给该引用

在 JVM 中有可能发生指令重排。考虑这样一种情况,线程 1 先执行 new Singleton (),发生指令重排,先做了第 1 步,第 3 步;此时线程 2 执行 if (instance ==null) 语句后直接返回了该对象的引用,但返回的是一个未经初始化的对象的引用,使用该引用就会发生 Error。解决方法也很简单,加上 volatile 关键字即可。

class Singleton{  
	private volatile static Singleton instance = null;
    
	private Singleton(){}  
	  
	public static Singleton getInstance(){  
		if(instance ==null){
			synchronized(Singleton.class){
				if(instance == null){   
					instance = new Singleton();  
				}  
			}
		}
		return instance;  
	}  
}

保证指令不会发生重排。

面试官:你这又是同步,又是 volatile,那目前有没有什么简单且安全高效的写法?

小明:有的,我们可以使用枚举类型,利用 JVM 自身的特性,去保证单例模式的安全和高效

public enum Singleton {	
	/**
	 * 定义一个枚举的元素,它就代表了Singleton的一个实例
	 */
	uniqueInstance;
	
}