1. 什么是单例
单例模式是指在java应用中,能够保证一个类只有一个对象实例,并提供一个访问该实例的全局访问点。
2. 应用场景
- 比如Windows系统的任务管理器,不管打开多少次任务管理器,只会弹出一个窗口。如果不使用单例机制,将弹出多个窗口,如果这些窗口显示的内容完全一致,则是重复对象,浪费内存资源;如果这些窗口显示的内容不一致,则意味着在某一瞬间系统有多个状态,与实际不符。
- 还有windows的回收站,在整个系统中,回收站一直维护着仅有的一个实例。
3. 实现方式
很多博客里都有实现单例的案例,这里推荐三种比较安全的实现方式:双重校验锁法、静态内部类法和枚举法。
3.1 双重校验锁法
特点:
线程安全(双重检测判断),效率高(因为这里只需要在第一次创建对象时同步,一旦创建成功,以后获取实例的时候就不需要再同步了),可以延时加载(显而易见,在真正调用的时候才创建对象)。
关键点:
一.私有构造函数
二.声明静态单例对象
三.构造单例对象之前要加锁
四.需要两次检测单例实例是否已经被构造,分别在锁之前和锁之后
实现代码:
public class Singleton2 {
//加上volatile关键字禁止java内存模型的指令重排序机制
private static volatile Singleton2 instance;
private Singleton2() {}
//只对需要锁的部分代码加锁
public static Singleton2 getInstance() {
if (null == instance) {
//只需在第一次创建对象的时候加同步块,执行效率高
synchronized (Singleton2.class) {
//双重判断,并发情况下保证只有一个实例
if (null == instance) {
instance = new Singleton2();
}
}
}
return instance;
}
public static void main(String[] args) {
Singleton2 s1 = Singleton2.getInstance();
Singleton2 s2 = Singleton2.getInstance();
System.out.println(s1);
System.out.println(s2);
System.out.println(s1 == s2);
}
}
输出结果:
com.test.singleton.Singleton2@70dea4e
com.test.singleton.Singleton2@70dea4e
true
结论:
确实是单例。
说明:
- 为何要检测两次?
如上面所述,有可能延迟加载或者缓存原因,造成构造多个实例,违反了单例的初衷。 - 构造函数能否公有化?
不行,单例类的构造函数必须私有化,单例类不能被实例化,单例实例只能静态调用 - lock住的对象为什么要是object对象,可以是int吗?
不行,必须锁引用类型。如果锁值类型,每个不同的线程在声明的时候值类型变量的地址都不一样,那么上个线程锁住的东西下个线程进来会认为根本没锁,相当于每次都锁了不同的门,没有任何用。而引用类型的变量地址是相同的,每个线程进来判断是否被锁的时候都是判断同一个地址,相当于是锁在通一扇门,起到了锁的作用。
Volatile关键字语义:
当一个变量定义为volatile之后,它将具备两种特性,第一是保证此变量对所有线程的可见性;第二是禁止指令重排序优化。
双重检测锁在jdk1.5之前理论上的实现想法是完美的,但是实际上是行不通的,这是由于java的内存模型有一个指令重排序机制,可能会导致一个已存在却不完整的instance实例对象。JVM在创建对象时是一个非原子性操作,通过new关键字创建对象可以分为三个步骤:
1.为instance分配内存空间;
2.利用构造器初始化对象;
3.将instance引用指向刚分配的内存地址(执行完这步instance就为非空了)。
这个过程可能发生指令重排序,也就是说上面三个步骤可能会打乱顺序,但不是说指令任意重排序,CPU需要正确处理指令依赖情况以保证程序能得出正确的执行结果,在当前情况下,指令2依赖于指令1,所以1,2的顺序不可能变,但是指令3并不依赖于指令2,所以可能会出现这样一种情况,执行1之后,再执行3,最后才执行2。这种情况不仅是可能的,而是有一些JIT编译器真实发生的现象。了解了指令重排机制之后,我们再回头看上面代码,如果没有volatile关键字,当线程A执行new Singleton()创建对象,并且将instance引用指向这个对象在内存的地址(这时instance非空),但是Singleton构造函数并没有执行,也就是说步骤1,3已经执行完毕,步骤2还没有执行,同时线程A被线程B占领,此时B得到的会是一个不完整的对象(未被初始化的对象),判断不为空,直接return instance,从而导致系统崩溃。
3.2 静态内部类法
特点:
使用静态内部类的好处是,静态内部类不会在单例加载时就加载,而是在调用getInstance()方法时才进行加载,达到了类似懒汉模式的效果,而这种方法又是线程安全的。
实现代码:
public class Singleton {
private static class SingletonHolder {
private static Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
public static void main(String[] args) {
System.out.println(Singleton.getInstance());
System.out.println(Singleton.getInstance());
System.out.println(Singleton.getInstance() == Singleton.getInstance());
}
}
输出结果:
com.test.singleton.Singleton@70dea4e
com.test.singleton.Singleton@70dea4e
true
结论:
确实是单例。
3.3 枚举法
《Effective Java》作者Josh Bloch 提倡的方式,可以说是来自神的写法。
特点:
简洁(利用枚举独特机制可以很简单的实现单例),线程安全(枚举底层是由static修饰的,静态资源初始化过程也是天然安全的),可以防止反射(阅读Constructor的newInstance()方法可以看到反射调用构造器创建对象的时候会先判断是否为枚举对象,如果是,就会抛出异常(“cannot reflectivy create enum object”),反射失败)和反序列化(枚举对序列化处理可以参考http://www.hollischuang.com/archives/197这篇文章)来破坏单例机制。不能延时加载(静态资源在类加载的时候自动加载
实现代码:
public enum Singleton3 {
//声明一个枚举对象,枚举本身就是单例
INSTANCE;
public static void main(String[] args) {
System.out.println(Singleton3.INSTANCE);
System.out.println(Singleton3.INSTANCE);
System.out.println(Singleton3.INSTANCE == Singleton3.INSTANCE);
}
}
输出结果:
INSTANCE
INSTANCE
true
结论:
确实是单例
总结:
通过以上几种实现方式,我们可以知道在运用单例模式往往要考虑以下几个性能:
- 是否能够延时加载,充分利用资源
- 是否线程安全
- 并发情况下的访问性能
- 是否可以防止反射和反序列化漏洞