前言:
在软件开发中,各个模块之间如何组织能使我们的系统更加优雅健壮,来保证代码具有良好的复用性,扩展性以及可读性,这对软件设计人员至关重要。于是经过前人反复推敲,论证,使用之后被大多数人知晓,理解形成一套设计模式体系(一种套路,或者武功秘籍(比如葵花宝典、九阴真经)),并由Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 合著(1995年,简称‘四人帮’书或GOF23设计模式),今天介绍里面一种常用的设计模式——单例模式。
定义:
在java应用中,单例模式能够保证一个类只有一个对象实例,并提供一个访问该实例的全局访问点。
应用场景:
1.比如Windows系统的任务管理器,不管打开多少次任务管理器,只会弹出一个窗口。如果不使用单例机制,将弹出多个窗口,如果这些窗口显示的内容完全一致,则是重复对象,浪费内存资源;如果这些窗口显示的内容不一致,则意味着在某一瞬间系统有多个状态,与实际不符。
2.还有windows的回收站,在整个系统中,回收站一直维护着仅有的一个实例。
3.再比如我们的文件系统,一个操作系统只有一个文件系统。
实现思想:
构造器私有化(无法通过构造器来创建对象,只能通过静态方法获取实例对象),自身维护一个实例对象(static修饰,也是私有的,确保整个系统只有一个实例),提供一个获取实例的静态方法(提供全局访问点)。
实现方式:
单例模式的几种实现方式(由于反射和反序列化可以破坏单例模式,这里先不考虑,)。
一、饿汉式单例模式(可用)
public class Singleton{
//类加载的时候初始化对象,天然的线程安全,不能延时加载
private static Singleton instance = new Singleton();
//构造器私有
private Singleton(){
}
//通过静态方法获取实例对象
public static Singleton getInstance(){
return instance;
}
}
特点:线程安全(由于类加载机制,JVM能够保证在加载类的时候免疫许多由多线程引起的问题,所以是天然安全的),获取实例不用考虑加锁,对应的就效率高,但是不能延时加载,不管用不用这个对象,都会实例化(可能会造成资源浪费)。
二、懒汉式单例模式(可用)
public class Singleton{
//类加载的时候不初始化对象,什么时候用什么时候初始化
private static Singleton instance;
private Singleton(){
}
//加同步锁保证线程安全,调用效率低,真正用的时候才创建对象,延时加载
public static synchronized Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
特点:线程安全(必须加锁,不然当instance==null的时候两个线程可以并发的进入if语句从而会创建多个对象,违反了单例初衷),执行效率低,可以延时加载,真正用的时候再创建对象(资源利用率高)。
三、双重检测锁(推荐)
public class Singleton{
//加上volatile关键字禁止java内存模型的指令重排序机制
private static volatile Singleton instance = null;
private Singleton(){
}
//只对需要锁的部分代码加锁
public static Singleton getInstance(){
if(instance == null){
//只需在第一次创建对象的时候加同步块,执行效率高
synchronized(Singleton.class)
//双重判断,并发情况下保证只有一个实例
if(instance == null){
instance = new Singleton();
}
}
return instance;
}
}
特点:线程安全(双重检测判断),效率高(因为这里只需要在第一次创建对象时同步,一旦创建成功,以后获取实例的时候就不需要再同步了),可以延时加载(显而易见,在真正调用的时候才创建对象)
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,从而导致系统崩溃。
Volatile屏蔽指令重排序的语义是在jdk1.5的时候才完全修复,此前即使将变量定义为volatile也不能完全避免重排序所导致的问题,这点也是在jdk1.5之前的java中无法安全的使用双重检测锁来实现单例模式的原因。
四、静态内部类实现单例(推荐)
public class Singleton{
//加载Singleton类的时候不会加载静态内部类,可以实现延时加载
private static class Inner{
//静态内部类维护一个实例对象
private static Singleton instance = new Singleton();
}
private Singleton(){
}
//类加载的时候天然线程安全,不用同步锁,调用效率高
public static Singleton getInstance(){
//调用的时候才会加载内部类
return Inner.instance;
}
}
特点:延时加载(加载外部类的时候并不会加载内部类,真正调用getInstance()的时候才会加载),线程安全(同样类加载的时候天然线程安全,所以不用同步,执行效率高)。
通过代码测试加载外部类的时候静态内部类是否会被加载
public class Singleton{
//加载Singleton类的时候不会加载静态内部类,可以实现延时加载
private static class Inner{
//静态内部类维护一个实例对象
private static Singleton instance = new Singleton();
static{
System.out.println("内部类已被加载");
}
}
private Singleton(){
}
//类加载的时候天然线程安全,不用同步锁,调用效率高
public static Singleton getInstance(){
//调用的时候才会加载内部类
return Inner.instance;
}
static{
System.out.println("外部类已被加载");
}
}
在外部类和内部类都加上一个静态代码块,我们知道加载类的时候static块代码也会加载,写个测试类看下控制台结果
public class Test{
public static void main(String[] args) {
try{
Class clazz = Class.forName("com.ahua.singleton.Singleton");
}catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
这里利用反射加载了Singleton类,外部内的静态块已执行,很明显内部类是不会被加载的。
五、枚举实现单例(推荐)
public enum Singleton{
//声明一个枚举对象,枚举本省就是单例
INATNCE;
}
特点:简洁(利用枚举独特机制可以很简单的实现单例),线程安全(枚举底层是由static修饰的,静态资源初始化过程也是天然安全的),可以防止反射(阅读Constructor的newInstance()方法可以看到反射调用构造器创建对象的时候会先判断是否为枚举对象,如果是,就会抛出异常(“cannot reflectivy create enum object”),反射失败)和反序列化(枚举对序列化处理可以参考http://www.hollischuang.com/archives/197这篇文章)来破坏单例机制。不能延时加载(静态资源在类加载的时候自动加载)
总结:
通过以上几种实现方式,我们可以知道在运用单例模式往往要考虑以下几个性能:
- 是否能够延时加载,充分利用资源
- 是否线程安全
- 并发情况下的访问性能
- 是否可以防止反射和反序列化漏洞
参考资料:
《深入理解JVM》
《大话设计模式》