最近在看三石-道的关于并发的一些博客,在一篇博客中看到了基于DCL实现的单例模式:
http://www.molotang.com/articles/407.html
于是在并发编程网上又看了两篇博客,以加深对单例模式的理解。
http://ifeve.com/doublecheckedlocking/
http://ifeve.com/syn-jmm-pre/
首先总结一下实现单例模式的几种办法:
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 = null;
private Singleton() {
}
public static synchronized Singleton getInstance() {
if (instance == null)
instance = new Singleton();
return instance;
}
}
3:DCL式(double checking lock),为了避免取得过期对象而想出来的:
public class Singleton {
private static volatile Singleton instance = null;
private Singleton() {
}
public static Singleton instance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
DCL式的会有很多问题,在《Java并发编程实践》一书中的16.2.4一节对其进行了说明:问题可能出来线程可能看到部分创建的对象(存在重排的可能性),打算有时间了写个例子验证一下。
4:解决了DCL的缺陷,由Bill Pugh提出:不仅实现了延迟加载而且可以实现同步:
public class Singleton {
private Singleton() {
}
private static class SingletonHolder {
public static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
5:《Effective Java》上推荐的,采用enum:
public enum Singleton {
INSTANCE;
void show(){System.out.println("Singleton");}
}
//客户端调用:
public class Test {
public static void main(String args[]){
Singleton.INSTANCE.show();
}
}
enum和class不同,enum的成员函数和成员变量默认都是public的,而class默认都是private的。
6:还可以用线程局部存储来修复DCL,每个线程各自保存一个flag来表示该线程是否执行了同步。
class Foo {
/** If perThreadInstance.get() returns a non-null value, this thread
has done synchronization needed to see initialization
of helper */
private final ThreadLocal perThreadInstance = new ThreadLocal();
private Helper helper = null;
public Helper getHelper() {
if (perThreadInstance.get() == null) createHelper();
return helper;
}
private final void createHelper() {
synchronized(this) {
if (helper == null)
helper = new Helper();
}
// Any non-null value would do as the argument here
perThreadInstance.set(perThreadInstance);
}
}
但是,如果我们采用序列化/烦序列化的时候,单例可能会产生多个对象。这样单例就失败了。
当然了,采用enum实现单例的话是不用担心序列化/反序列化的,因为readObject和writeObject两个Serializable接口中的方法对enum是单独处理的。
如果没有采用enum实现单例,又遇到这个类实现了Seriaalizable接口需要序列化/反序列化的情况也不要担心,只要重写readResolve方法就可以了,在readResolve方法中返回之前对象的引用。
像这样:
public class Singleton implements Serializable {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {
}
private Object readResolve() {
//这个很重要
return INSTANCE;
}
public static Singleton getInstance() {
return INSTANCE;
}
}
当进行反序列化时, ois.readObject();时内部就是通过反射检查implements Serializable的类有没有readResolve方法,如果有就把readResolve的返回值作为ois.readObject();的返回值. 所以readResolve必须返回之前对象的引用。