单例模式主要有如下几个关键点:
(1)构造函数对外不开放,一般为Private;
(2)通过一个静态方法或者枚举返回单例类对象;
(3)单例类的对象确保有且只有一个(特别是在多线程环境下);
(4)单例类对象在反序列化时确保不会重新构建对象。
以下是几种常见的单例模式:
1、饿汉式单例
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton () {}
public static Singleton getInstance() {
return instance
}
}
优点:是线程安全的,在类创建之初就已经创建好一个静态的对象供系统使用,以后不在改变。
缺点:未达到懒加载的目的,还没有使用就已经占用内存了。
2 、懒汉式单例
public class Singleton {
private static Singleton instance;
private Singleton () {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton ();
}
return instance;
}
}
优点:是线程安全的,是单例只有在使用时才会被实例化,在一定程度上节约了资源,实现了懒加载,但一般不建议使用,很诧异吧?因为下面的缺点太大了
缺点:在第一次加载时需要及时进行实例化,反应稍慢,最大的问题是每次调用 getInstance都进行同步,造成不必要的同步开销。所以不建议使用
3、双重校验锁单例(DoubleCheckLock-简拼DCL)
public class Singleton {
private static Singleton sInstance = null;
private Singleton() {}
public static Singleton getInstance() {
if (mInstance == null) {
synchronized (Singleton.class) {
if (mInstance == null) {
sInstance = new Singleton();
}
}
return sInstance;
}
}
优点:是线程安全的,两次判空既避免不必要的同步,又在对象是null的情况下创建实例,实现了懒加载,资源利用率高。
缺点:第一次加载时反应稍慢,由于 Java 内存模型的原因偶尔会失败。在高并发环境下也有一定的缺陷,虽然发生概率很小。
注意:假设线程执行到sInstance = new Singleton()语句时,它大致做了 3 件事情:
(1)给 Singleton 的实例分配内存;
(2)调用 Singleton 的构造函数,初始化成员字段;
(3)将 sInstance 对象指向分配的内存空间(这时sInstance就不是null了)。
但是由于Java编译器允许处理器乱序执行,以及JDK1.5之前JMM(Java Memory Model,即Java内存模型)中Cache、寄存器到主内存回写顺序的规定,上面的第二和第三的顺序是无法保证的。也就是说,执行顺序可能是1-2-3也可能是1-3-2。如果是后者,并且在3执行完毕、2未执行之前,被切换到线程B上,这时候sInstance因为己经在线程A内执行过了第三点,sInstance己经是非空了,所以,线程B直接取走sInstance,再使用时就会出错,这就是DCL失效问题。
在JDK1.5之后,SUN官方己经注意到这种问题,调整了JVM,具体化了volatile关键字,因此,如果JDK是1.5或之后的版本,只需要将sInstance的定义改成private volatile static Singleton sInstance = null就可以保证sInstance对象每次都是从主内存中读取,就可以使用DCL的写法来完成单例模式。当然,volatile 或多或少也会影响到性能,但考虑到程序的正确性,牺牲这点性能还是值得的。
DCL 模式是使用最多的单例实现方式,它能够在需要时才实例化单例对象,并且能够在绝大多数场景下保证单例对象的唯一性,除非你的代码在并发场景比较复杂或者低于JDK 6版本下使用,否则,这种方式一般能够满足需求。*
4、静态内部类单例模式
public class Singleton {
private Singleton() { }
public static Singleton getInstance () {
return SingletonHolder.sInstance;
}
/**
* 静态内部类
*/
private static class SingletonHolder {
private static final Singleton sInstance = new Singleton();
}
}
优点:不仅是线程安全,也保证了单例对象的唯一性,同时还延迟了单例的实例化。最最最重要的是弥补了上面双重校验锁在极端情况下失效的问题。这是最推荐使用的方式。
缺点:懒加载嘛,第一次初始化慢,这个好像是通病吧!
5、枚举单例
public enum SingletonEnum {
INSTANCE;
}
很简洁有木有?很间接是不是?很简洁对不对?
优点:写法简单是枚举单例最大的优点,最重要的是枚举实例的创建默认就是线程安全的,并且在任何情况下(包括反序列化)它都是一个单例。
缺点:写法虽简单,但是晦涩难懂不易理解
注意:在上述的几种单例模式实现中【除了枚举】,在一个情况下它们会出现重新创建对象的情况,那就是反序列化。
通过序列化可以将一个单例的实例对象写到磁盘,然后再读回来,从而有效地获得一个实例。即使构造函数是私有的,反序列化时依然可以通过特殊的途径去创建类的一个新的实例,相当于调用该类的构造函数。反序列化操作提供了一个钩子函数,类中具有一个私有的、被实例化的方法readResolve(),这个方法可以让开发人员控制对象的反序列化。例如,上述几个示例中如果要杜绝单例对象在被反序列化时重新生成对象,那么必须加入readResolve方法并且将sInstance对象返回,而不是默认的重新生成一个新的对象。而对于枚举,并不存在这个问题,因为即使反序列化它也不会重新生成新的实例。
private Object readResolve() throws ObjectStreamException {
return sInstance;
}
- 另外:还有利用容器实现单例,不常用就不做阐述了。
总结:枚举写法最简单,内部类最易懂实用,双重校验锁偶尔会失灵,剩下懒汉和饿汉都不是好汉,弃用便好!