饿汉式
这种方法非常简单,因为单例的实例被声明成 static 和 final 变量了,在第一次加载类到内存中时就会初始化,所以创建实例本身是线程安全的
public class Singleton {
// 类加载时就初始化
private static final Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance(){
return instance;
}
}
缺点是它不是一种懒加载模式(lazy initialization),单例会在加载类后一开始就被初始化,即使客户端没有调用 getInstance()方法。饿汉式的创建方式在一些场景中将无法使用:譬如 Singleton 实例的创建是依赖参数或者配置文件的,在 getInstance() 之前必须调用某个方法设置参数给它,那样这种单例写法就无法使用
懒汉式,线程不安全
public class Singleton {
private static Singleton instance;
private Singleton () {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
这段代码简单明了,而且使用了懒加载模式,但是却存在致命的问题。当有多个线程并行调用 getInstance() 的时候,就会创建多个实例
懒汉式,线程安全
为了解决上面的问题,最简单的方法是将整个 getInstance() 方法设为同步(synchronized)
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
虽然做到了线程安全,并且解决了多实例的问题,但是它并不高效。因为在任何时候只能有一个线程调用 getInstance() 方法。但是同步操作只需要在第一次调用时才被需要,即第一次创建单例实例对象时。这就引出了双重检验锁
双重检验锁
public class Singleton {
private volatile static Singleton instance;
private Singleton () {}
public static Singleton getSingleton() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
称其为双重检查锁,是因为会有两次检查 instance == null,一次是在同步块外,一次是在同步块内。为什么在同步块内还要再检验一次?因为可能会有多个线程一起进入同步块外的 if,如果在同步块内不进行二次检验的话就会生成多个实例了
而 instance = new Singleton() 这句,并非是一个原子操作,事实上在 JVM 中这句话做了下面 3 件事:
- 给 instance 分配内存
- 调用 Singleton 的构造函数来初始化成员变量
- 将 instance 对象指向分配的内存空间(执行完这步 instance 就为非 null 了)
但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错
所以需要将 instance 变量声明成 volatile
静态内部类
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton () {}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
静态内部类与外部类没有什么关系,外部类加载的时候,内部类不会被加载,静态内部类只是调用的时候用了外部类的名字而已,所以即使 Singleton 类被加载也不会创建单例对象
静态内部类 SingletonHolder 只有在 getInstance() 方法第一次被调用时,才会被加载,从而初始化它的静态域(创建 Singleton 的实例),因此该种方式实现了懒汉式的单例模式
不仅如此,根据 JVM 本身机制,由于是静态的域,因此只会在虚拟机装载类的时候初始化一次,并由虚拟机来保证它的线程安全性
同时不用 synchronized,所以没有性能缺陷
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return INSTANCE;
}
}
这个写法也是利用类的静态变量的唯一性,不过和上面的写法相比,不能实现懒加载
Enum
用枚举写单例最大的优点是简单
public enum Singleton {
INSTANCE {
@Override
protected void read() {
System.out.println("read");
}
@Override
protected void write() {
System.out.println("write");
}
};
protected abstract void read();
protected abstract void write();
}
以上是一个单例枚举的例子,要获取该实例只需要 Singleton.INSTANCE,并且此种方式可以保证该单例线程安全、防反射攻击、防止序列化生成新的实例
反编译后的类:
public abstract class Singleton extends Enum {
private Singleton(String s, int i) {
super(s, i);
}
protected abstract void read();
protected abstract void write();
public static Singleton[] values() {
Singleton asingleton[];
int i;
Singleton asingleton1[];
System.arraycopy(asingleton = ENUM$VALUES, 0, asingleton1 = new Singleton[i = asingleton.length], 0, i);
return asingleton1;
}
public static Singleton valueOf(String s) {
return (Singleton)Enum.valueOf(singleton/Singleton, s);
}
Singleton(String s, int i, Singleton singleton) {
this(s, i);
}
public static final Singleton INSTANCE;
private static final Singleton ENUM$VALUES[];
static {
INSTANCE = new Singleton("INSTANCE", 0) {
protected void read() {
System.out.println("read");
}
protected void write() {
System.out.println("write");
}
};
ENUM$VALUES = (new Singleton[] {
INSTANCE
});
}
}
看了这个类的真身后,可以知道:
- 枚举类实现其实省略了 private 类型的构造函数
- 枚举类的域其实是相应的 enum 类型的一个实例对象
- 枚举类的域会在 static 方法块中被实例化,也就是说在 enum 被类加载器加载时被实例化,并非懒加载
- enum 是 abstract 类,所以没法实例化,反射也无能为力
- 关于线程安全的保证,其实是通过类加载机制来保证的,INSTANCE 是在 static 块中实例化的,JVM 加载类的过程显然是线程安全的
- 而枚举可以反序列化是因为 Enum 实现了 readResolve 方法
对于一个标准的 enum 单例模式,最优秀的写法还是实现接口的形式:
// 定义单例模式中需要完成的代码逻辑
public interface MySingleton {
void doSomething();
}
public enum Singleton implements MySingleton {
INSTANCE {
@Override
public void doSomething() {
System.out.println("complete singleton");
}
};
public static MySingleton getInstance() {
return Singleton.INSTANCE;
}
}
当单例类被多个类加载器加载,如何还能保持单例
如果单例由不同的类装载器装入,那便有可能存在多个单例类的实例。假定不是远端存取,例如一些 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);
}
单例类防止反序列化
public class Singleton implements java.io.Serializable {
// ...
private Object readResolve() {
return INSTANCE;
}
}
readResolve() 方法可以理解反序列化过程的出口,就是在反序列化完成得到对象前,把这个对象换成我们确定好的那个
反射破坏单例原则
在单例模式中,只对外提供工厂方法(获取单例),并私有化构造函数,来防止外面多余的创建。对于一般的外部调用来说,私有构造函数已经很安全了。但是一些特权用户可以通过反射来访问私有构造函数,然后打开访问权限 setAccessible(true),就可以访问私有构造函数了,这样破坏了单例的私有构造函数保护,创建了一个新的实例。如果要防御这样的反射侵入,可以修改构造函数,加上第二次实例化的检查,当发生创建第二个单例的请求时会抛出异常
private static int cntInstance = 0;
private Singleton() throws Exception {
if (cntInstance++ > 1) {
throw new Exception("can't create another singleton instance.");
}
}