单例模式可以说是我们开发中用得最多的一种创建型设计模式了。当我们想控制某个类在内存中存在对象的个数;一个系统可以同时存在多个打印的任务,但是同时只能有一个对象被打印;一个系统只能有一个窗口或者一个资源管理器;一个程序只能有一个计数器时….我们都需要使用单例模式来解决这类问题。
我们常用的单例有饿汉式,懒汉式,DCL(双重效验锁),枚举实现和静态内部类这5种实现方式。
饿汉式
饿汉式,故名思义,因为饿嘛,所以在类被加载到内存的时候,对象就已经被创建好了,而不管是否马上需要使用它。其生命周期与类保持一致。
饿汉式通用写法
下面是饿汉式的通用写法:
public class SingleTon {
public static final SingleTon single = new SingleTon();
private SingleTon() {
}
public static SingleTon getInstance() {
return single;
}
}
饿汉式的缺点
- 在类加载时,完成了对象的初始化,所以类的加载稍慢,但对象的加载是非常快的,是一个以空间换时间的做法。
- 当类加载完成时,对象也就被创建并缓存在内存里了,不管是否需要使用,不满足按需使用的原则。
懒汉式
懒汉式,因为它懒,所以,在类被加载的时候,并没有立刻创建对象,而是当需要时,调用相应方法时,才会创建对象,与饿汉式刚好相反。类的加载比较快,而对象的加载会慢点,因为要在调用方法时,才会去new对象。
懒汉式的通用写法
public class SingleTon {
public static SingleTon single = null;
private SingleTon() {
}
public static SingleTon getInstance() {
if (single == null) {
single = new SingleTon();
}
return single;
}
}
懒汉式的缺点
- 懒汉式是以时间换空间的一种做法,真正需要的时候,才去创建对象,创建对象也是需要时间的,所以,会比饿汉式获取对象的时候稍慢。
- 多线程访问是不安全的,因为创建对象时需要时间的,正在创建对象的时候,如果有其它线程也执行了
getInstance
方法,而对象还没有创建成功的话,上面的判断就失去了作用,而让其它线程也创建了该类的实现。- 对于2的做法可以对方法使用
synchronized
方法同步的方式,来做线程同步,而这样做,效率比较低。
DCL(双重效验锁机制)
在懒汉式的基础上做了对多线程的支持,通过两重判断和同步代码块来保证多线程的同步问题。99.99%的多线程环境下,都不会有问题。而如果出现问题,也是很难重现,以及排查错误的。
DCL的通用写法
public class SingleTon {
public static SingleTon single = null;
private SingleTon() {
}
public static SingleTon getInstance() {
if (single == null) {
synchronized (SingleTon.class) {
if (single == null) {
single = new SingleTon();
}
}
}
return single;
}
}
这里的两层判断分别代码的意思:
- 第一层判断主要是为了避免不必要的同步。也就是说当对象已经创建了,就不必再执行同步代码块来消耗不必要的时间了。
- 第二层主要是为了当对象为null时创建该对象的实例。
DCL的缺点
- DCL并没有明显的缺点,它是一个综合的结果。但是在高并发的情况下,会有一定机率出现问题。
为什么DCL的写法存在问题
我们先来说说创建对象时,jvm的大致工作。
- 给SingleTon的实例分配内存。
- 初始化SingleTon的构造器。
- 将instance对象指向分配的内存空间
由于java
编译器允许处理器乱序执行(out-of-order),以及JDK1.5之前JMM(java memory model)
中Cache
、寄存器到主内存回写的顺序,上面的第二点和第三点无法得到保证。好就是说,有可能执行的顺序是123,或者132。而如果是执行132的话,当一个线程执行的顺序是132时,当它执行到了3,这时,instance对象已经指定了1中分配的地址,已经不为null了,而此时另一个线程正好执行同样的代码时,因为instance
已经不为null
了,所以,就直接使用了此对象,结果,因为此对象根本还未创建完毕,而导致报错。
这样的问题是很难被发现的。实际上在java1.5
之后,调整了JMM
,具体化的volatile
关键字。因此只要在java1.5
之后,将instance
定义成private volatile static SingleTon singleTon = null
.就可以保证每次获取instance
都去主内存里读取,就可以解决DCL
带来的问题。
优化后的DCL代码
public class SingleTon {
public volatile static SingleTon single = null;
private SingleTon() {
}
public static SingleTon getInstance() {
if (single == null) {
synchronized (SingleTon.class) {
if (single == null) {
single = new SingleTon();
}
}
}
return single;
}
}
静态内部类实现
静态内部类的实现,实际上是对DCL
的另一种写法,它规避了DCL
可能带来的潜在麻烦,而又拥有DCL
按需加载,以及多线程安全的优点。是一种比DCL
更好的实现方式。一般会推荐此写法。
静态内部类通用写法
public class SingleTon {
private static class Inner {
public static final SingleTon single = new SingleTon();
}
private SingleTon() {
}
public static SingleTon getInstance() {
return Inner.single;
}
}
静态内部类的缺点
无
反射的问题
以上几种实现,实际上都不能保证对象在内存中的唯一性,因为有反射的存在。当使用反射时,上面的实现都将失去其作用。
如果使用上面的方法使用单例,只能靠代码规范来约束使用者了。而如果只在内部使用的话,我们通过代码规范一般是可行的。但如果是对外的第三方接入系统的话,可能有一定机率出现问题。
那有没有一种真正的能保证内存中单一实例的实现方式呢?有,使用枚举。
枚举实现单例
由于枚举的实现问题,造就了其天然支持多线程并发的特性,我们用其来实现单例是最好不过的事情了。
public enum SingleEnum {
SingleTon {
private int a = 0;
@Override
public SingleEnum getSingleTon() {
return SingleTon;
}
/**
* 测试方法
*/
public void testMethod() {
}
};
public abstract SingleEnum getSingleTon();
private SingleEnum() {
}
}
每一个枚举变量,实际上都是由类所组成的。而每个类里面即可以写变量,也可以写方法。所以,使用枚举来实现单例,也是一个不错的做法。
关于单例这种设计模式的几种写法,就写到这里了,在这里我推荐使用第四种静态内部类的方式来实现单例,而高并发的极端情况,如果不嫌麻烦,也可以使用枚举的方式来实现。