单例模式
1. 使用场景
当希望整个JVM中仅有此类的一个实例时。
2. 实现方式
饿汉式A
class Singleton {
private static Singleton me = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return me;
}
}
要点
- 构造私有,别的类无法创建实例
- 在类加载时就创建本类唯一实例
- 因为类加载只执行一次,没有多线程并发问题
- 可以用反射破坏单例,此问题同样存在于下面几种懒汉式的实现
- 可以用反序列化破坏单例的,此问题同样存在于下面几种懒汉式的实现 (可解决)
饿汉式B
enum Singleton {
INSTANCE;
}
要点
- enum 本身特性保证了实例个数
- 没有多线程并发问题,没有反射破坏单例问题,没有反序列化方法破坏单例问题
懒汉式A
class Singleton {
private static Singleton me = null;
private Singleton() { }
public static synchronized Singleton getInstance() {
if( me == null ){
me = new Singleton();
}
return me;
}
}
要点
- 构造私有,别的类无法创建实例
- 在类加载时暂时不创建实例,而是在第一次用到时创建
- 如果首次使用 getInstance() 方法是多线程并发,必须添加synchronized 保证线程安全
- 此实现有缺陷:本来只是在首次使用 getInstance() 时才有并发问题,但这个实现是次次都需要synchronized 效率低下
懒汉式B
class Singleton {
private static volatile Singleton me = null;
private Singleton() { }
public static Singleton getInstance() {
if (me == null) {
synchronized (Singleton.class) {
if (me == null) {
me = new Singleton();
}
}
}
return me;
}
}
要点
- 构造私有,别的类无法创建实例
- 在类加载时暂时不创建实例,而是在第一次用到时创建
- 首次使用 getInstance() 才使用 synchronized 加锁,后续使用时无需加锁
- volatile 关键字是为了防止指令重排,只有在JDK 5 以上的版本有效
指令重排问题分析
首先要明白jvm会对代码进行优化,进行指令重排,例如
me = new Singleton();
实际可以分别三步:1) 分配空间,生成了引用地址 2) 调用构造方法 3) 将引用地址赋值给me变量
其中 2) 3) 两步的顺序不是固定的,也许jvm会优化为:先将引用地址赋值给me变量后,再执行构造方法。
如果两个线程t1, t2按如下时间序列执行:
时间1 t1 线程执行到 me = new Singleton(); 时间2 t1 线程分配空间,为Singleton对象生成了引用地址 时间3 t1 线程将引用地址赋值给me,这时me != null 时间4 t2 线程进入getInstance() 方法,发现 me != null,直接返回me 时间5 t1 线程执行Singleton的构造方法
这时t1 还未完全将Singleton的构造方法执行完毕,如果在Singleton的构造方法中要执行很多初始化操作,那么t2拿到的是很可能是一个不完整的单例。
问题解决
volatile 修饰的变量不会参与指令重排,因此上述问题得到解决,但要注意在JDK 5 以上的版本volatile才会真正发挥作用。
懒汉式C
class Singleton {
private Singleton() { }
private static class LazyHolder {
private static Singleton me = new Singleton();
}
public static Singleton getInstance() {
return LazyHolder.me;
}
}
要点
- 构造私有,别的类无法创建实例
- 在类加载时暂时不创建实例,而是在第一次用到时创建
- 使用了静态内部类是用到时才加载来实现懒汉实例化,并且因为内部类的加载只会执行一次,从而能保证线程安全
3. 不使用理由
单例模式由于其简单易实现,为初学者所广泛使用,最容易被误用和滥用。
推荐使用IOC容器(例如spring)来管理项目中的单例对象,相对而言,IOC容器内是容器内单例
,而单例模式是JVM内单例
。