Singleton Pattern单例模式

Important:推荐使用枚举方式实现单例模式!

Outlines
经典的单例模式
处理多线程:同步加锁创建
处理多线程:同步加锁创建(优化版:双重检查加锁double-checked locking)
静态初始化:非延迟创建
防止反射
枚举方式(实现单例模式推荐使用的方式)
e.g. 创建单例线程池

有些时候我们只想要实例化一个对象,比如线程池对象、缓存对象、日志对象以及充当打印机、显卡等设备的驱动程序的对象。事实上,这类对象也只能有一个实例,如果制造出多个实例,就会导致许多问题产生,例如程序的行为异常、资源使用过量、结果不一致等等。这个时候就需要我们使用Singleton Pattern单例模式了
单例模式:确保一个类只有一个实例,并提供一个全局访问点。

经典的单例模式

Java
public class Singleton {
	private static Singleton uniqueInstance;
	
	private Singleton() {}
	
	public static Singleton getInstance() {
		if(uniqueInstance == null) { //1
			uniqueInstance = new Singleton(); //2
		}
		return uniqueInstance;
	}
}

这个方式的一大弊端在于无法胜任多线程应用。比如现在有两个线程X和Y,其中X运行到了1的位置,Y运行到了2的位置,此时Y线程已经创建了一个实例,而下一个时刻,线程X又会执行一遍uniqueInstance = new Singleton(),相当于又创建了一个实例。

处理多线程:同步加锁创建

Java
public class Singleton {
	private static Singleton uniqueInstance;
	
	private Singleton() {}
	
	public static synchronized Singleton getInstance() { //1
		if(uniqueInstance == null) {
			uniqueInstance = new Singleton();
		}
		return uniqueInstance;
	}
}

我们在1处添加了synchronized关键字对整个函数进行加锁操作,保证同一时刻只有一个线程能够访问该函数,避免了线程不安全的问题。但是该方式的一大弊端在于会降低近100倍的效率。更糟糕的是,只有第一次执行时,才真正需要同步。即一旦设置好uniqueInstance变量后,就不再需要同步这个方法了,之后每次调用这个方法,同步都是一种累赘。该方式还可以进行优化,即只同步代码块。

处理多线程:同步加锁创建(优化版:双重检查加锁double-checked locking)

Java
public class Singleton {
	private static volatile Singleton uniqueInstance; //1 这里添加了volatile关键字
	
	private Singleton() {}
	
	public static Singleton getInstance() { //2 这里取消了synchronized关键字
		if(uniqueInstance == null) { //3 如果已经被实例化了,则直接返回
			synchronized (Singleton.class) { //4 同步代码块
				if(uniqueInstance == null) { //5 第二重检查
					uniqueInstance = new Singleton(); //6
				} //7
			} //8
		} //9
		return uniqueInstance;
	}
}

这里的5为什么要进行第二重检查呢?比如现在有两个线程X和Y,X运行到了3的位置,Y是第一个进入该函数的线程运行到了位置4;后面时刻,线程Y运行到了位置8(退出同步代码块),此时实例已经被创建了,线程X运行到位置4(进入同步块),如果不进行这第二重的判断,那么线程X又会再次创建一个实例。

这里的1为什么要添加关键字volatile?volatile的作用是告诉编译器不对代码的执行顺序进行优化,老老实实的执行。因为new Singleton()不是一个原子操作,具体比如是有三个步骤:
A申请一段内存空间;
B初始化相关变量;
C将引用指向这段内存空间(此时uniqueInstance才不为null)。
那么正常来说new Singleton()的执行顺序就是ABC,但是编译器为了提高效率,会对代码的执行顺序进行优化,比如优化成ACB顺序。如果new Singleton()按照ACB的顺序,那么问题就来了,比如线程Y执行new Singleton()到AC的时候,这个时候实例已经不为空了,但是由于B还没有执行,所以该实例还并未完全初始化,如果这个时候线程X运行到了2,那么它就不能运行到位置3,而是直接到位置9,然后返回该实例,就可能造成程序的不稳定或者其他异常情况发生。

静态初始化:非延迟创建

Java
public class Singleton {
	private static Singleton uniqueInstance = new Singleton(); //在静态初始化阶段就创建实例
	
	private Singleton() {}
	
	public static Singleton getInstance() {
		return uniqueInstance;
	}
}

防止反射

前面提到的方式都还存在一个问题:利用反射机制,可以调用私有方法,从而再次创建新的实例。

Java
public class Singleton {
	private static Singleton uniqueInstance = new Singleton(); //在静态初始化阶段就创建实例
	
	private Singleton() {}
	
	public static Singleton getInstance() {
		return uniqueInstance;
	}
	
	public static void main(String arg[]) throws Exception{
		Singleton fisrtObject = Singleton.getInstance();
		
		Class objectClass = Singleton.class; //利用反射机制
    Constructor constructor = objectClass.getDeclaredConstructor();
    constructor.setAccessible(true); //获取私有构造器的访问权限
    Singleton secondObject = (Singleton) constructor.newInstance(); //创建第二个实例
    
    System.out.println(fisrtObject);
    System.out.println(secondObject);
    System.out.println(fisrtObject == secondObject);
	}
}
runs.wang.test.Singleton@15db9742
runs.wang.test.Singleton@6d06d69c
false

不难发现,如果我们通过反射机制可以利用私有的构造器,进而创建新的实例,破坏掉单例模式。所以在我们上面的方式中,需要防止反射,具体地,可以对私有构造函数进行限制。

Java
	private Singleton() {
		if(uniqueInstance != null) { //防止反射
			throw new RuntimeException();
		}
	}

所以,正确实现单例模式的方式如下:

静态初始化方式

Java
public class Singleton {
	private static Singleton uniqueInstance = new Singleton();
	
	private Singleton() {
		if(uniqueInstance != null) {
			throw new RuntimeException();
		}
	}
	
	public static Singleton getInstance() {
		return uniqueInstance;
	}
}

同步加锁方式

Java
public class Singleton {
	private static Singleton uniqueInstance;
	
	private Singleton() {
		if(uniqueInstance != null) {
			throw new RuntimeException();
		}
	}
	
	public static synchronized Singleton getInstance() {
		if(uniqueInstance == null) {
			uniqueInstance = new Singleton();
		}
		return uniqueInstance;
	}
}

同步加锁方式(优化版)

Java
public class Singleton {
	private static volatile Singleton uniqueInstance; 
	
	private Singleton() {
		if(uniqueInstance != null) {
			throw new RuntimeException();
		}
	}
	
	public static Singleton getInstance() { 
		if(uniqueInstance == null) { 
			synchronized (Singleton.class) { 
				if(uniqueInstance == null) { 
					uniqueInstance = new Singleton(); 
				} 
			} 
		} 
		return uniqueInstance;
	}
}

枚举方式(实现单例模式推荐使用的方式)

Java
public enum Singleton {
	uniqueInstance;
}

e.g. 创建单例线程池

Java
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public enum ThreadManager {
	uniqueInstance;
	
	private final int coreThreadNum = 16;
	private final int maxThreadNum = 128;
	private final long threadAliveTime = 120L;
	private final TimeUnit timeUnit = TimeUnit.SECONDS;
	private final ArrayBlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(1024);
	private final ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
			coreThreadNum, 
			maxThreadNum, 
			threadAliveTime, 
			timeUnit, 
			workQueue
	);

	public ThreadPoolExecutor getThreadPool() {
		return threadPool;
	}
	
	public void execute(Runnable runnable) {
		threadPool.execute(runnable);
	}
	
	public <T> Future<T> submit(Callable<T> callable){
		return threadPool.submit(callable);
	}
	
}

Reference
《Head First Design Patterns》