在啃Spring的源码时,看到Spring IoC容器实例化Bean中采用了单例模式,学习一下单例模式。
在单例模式下要用私有构造器:
私有构造器,就是用private关键字声明的构造器。与一般公有构造器最大的区别在于,其访问权限是private,它只能被包含它的类自身所访问,而无法在类的外部调用,故而可以阻止外部实例化对象。
public class Singleton {
private static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
这是一个线程不安全的单例,因为如果多个线程能够同时进入 if (uniqueInstance == null) ,并且此时 uniqueInstance 为 null,那么多个线程会执行 uniqueInstance = new Singleton(); 语句,这将导致多次实例化 uniqueInstance。
可以对getUniqueInstance() 方法加锁:
public class Singleton {
private static Singleton uniqueInstance;
private Singleton() {
}
public static synchronized Singleton getUniqueInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
这样有一个问题,就是当一个线程进入该方法之后,其它线程试图进入该方法都必须等待,因此性能上有一定的损耗。
采用双重校验锁先判断 uniqueInstance 是否已经被实例化,如果没有被实例化,那么才对实例化语句进行加锁。
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
if (uniqueInstance == null) {
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
如果只有单层判断:
if (uniqueInstance == null) {
synchronized (Singleton.class) {
uniqueInstance = new Singleton();
}
}
在 uniqueInstance == null 的情况下,如果两个线程同时执行if语句,那么两个线程就会同时进入if语句块内。虽然在if语句块内有加锁操作,但是两个线程都会执行uniqueInstance = new Singleton(); 这条语句,只是先后的问题,也就是说会进行两次实例化,从而产生了两个实例。
注意一定要加volatile关键字,原因可以参考博主总结的这篇文章。
Effective Java作者Josh Bloch 提倡使用枚举的方式,因为创建一个enum类型是线程安全的。这种方法在功能上与公有域方法相近,但是它更加简洁,无偿提供了序列化机制,绝对防止多次实例化,即使是在面对复杂序列化或者反射攻击的时候。
public enum Singleton {
uniqueInstance;
}
关于枚举类的详解,可以参考这篇文章。