一、饿汉单例
1. 静态变量实现
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
这种实现方式基于class loader机制避免了多线程的同步问题。不过,instance在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用getInstance
方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance显然没有达到lazy loading的效果。
2. 静态代码块实现
public class Singleton {
private Singleton instance = null;
private Singleton() {}
static {
instance = new Singleton();
}
public static Singleton getInstance() {
return this.instance;
}
}
这种实现方式表面上看起来与上面差别挺大的,其实跟上面的差别不大,都是在类初始化即实例化instance。
二、懒汉单例
1. 基本实现,线程不安全
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
这种是最基本的lazy loading实现的单例模式,但是缺点很明显,在多线程下显然不能安全工作。
2. synchronized同步静态方法,线程安全
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}
这种写法能够很好的在多线程中工作,不会出现并发安全问题,也实现了懒加载。但是效率很低,synchronized使得这里并行变成串行。所以这种写法一般不会被使用到。
3. 静态内部类实现,推荐使用
public class Singleton {
// === 静态内部类 ===
private static class SingletonHolder {
private static final INSTANCE = new Singleton();
}
private Singleton() {}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
这种方式同样利用了class loader 的机制来保证初始化instance时只有一个线程,它跟上面提到的饿汉单例不同的是(很细微的差别):上面的饿汉单例是只要Singleton类被装载了,那么instance就会被实例化(没有达到lazy-loading效果)。而这种方式是Singleton类被装载了,instance不一定被初始化。因为SingletonHolder
类没有被主动使用,只有显示调用getInstance()
方法时,才会显示装载SingletonHolder
类,从而实例化instance。
4. 双重校验锁实现,推荐使用
public class Singleton {
// ===1:volatile修饰
private volatile static Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
// ===2:减少不必要同步,性能优化
if (singleton == null) {
// ===3:同步,线程安全
synchronized(Singleton.class) {
if (singleton == null) {
// ===4:创建singleton对象
singleton = new Singleton();
}
}
}
return singleton;
}
}
-
为什么要使用volatile修饰?
虽然已经使用synchronized进行同步,但是第四步创建对象时,会有下面的伪代码:
memory=allocate(); // 1. 分配内存空间 ctorInstance(); // 2. 初始化对象 singleton=memory; // 3. 设置singleton指向刚排序的内存空间
当线程A在执行上面伪代码时,2和3可能会发生重排序,因为重排序并不影响运行结果,还可以提升性能,所以 JVM 是允许的。如果此时伪代码发生重排序,步骤为1 -> 3 -> 2,线程A执行到第3步时,线程B调用
getInstance()
方法,在判断singleton == null
时不为null
,则返回singleton
。但此时singleton
并没有初始化完毕,线程B访问的将是个还没初始化完毕的对象。当声明对象的引用为volatile后,伪代码的2、3的重排序在多线程中将被禁止!
5. 枚举实现,推荐使用
public class Singleton {
private Singleton(){}
// === 延迟加载
private enum EnumHolder {
INSTANCE;
private static Singleton instance = null;
private Singleton getInstance() {
instance = new Singleton();
return instance;
}
}
public static Singleton getInstance() {
return EnumHolder.INSTANCE.instance;
}
}
三、问题注意
1. 不同类加载器加载
如果单例由不同的类加载器加载,那便有可能存在多个单例类的实例。假定不是远端存取,例如一些Servlet容器对每个Servlet使用完全不同的类加载器,这样的话如果有两个Servlet访问一个单例类,它们就都会有各自的实例。
可以用下面方式对这个问题进行修复:
private static Class getClass(String classname) throws ClassNotFoundException {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
if(classLoader == null) {
classLoader = Singleton.class.getClassLoader();
}
return (classLoader.loadClass(classname));
}
2. 序列化问题
如果Singleton实现了java.io.Serializable
接口,那么这个类的实例就可能被序列化和复原,不管怎样,如果你序列化一个单例类的对象,接下来复原多个那个对象,那你就会有多个单例类的实例。
public class Singleton implements java.io.Serializable {
public static Singleton INSTANCE = new Singleton();
protected Singleton() {}
private Object readResolve() {
return INSTANCE;
}
}