Java单例模式——暂时完美的单例

来源:互联网 发布:学车电销数据 编辑:程序博客网 时间:2024/06/07 17:39

最近在网上找资料研究了下单例模式,结合网上的资料记录下暂时完美的单例:

一、暂时完美单例

public class Singleton {    //私有构造方法,防止被实例化    private Singleton(){}    //此处使用一个内部类来维护单例    private static class SingletonFactory{        private static Singleton instance = new Singleton();    }    //获取实例    public static Singleton getInstance(){        return SingletonFactory.instance;    }    //如果对象被用于序列化,可以保证对象在序列化前后一致    public Object readResolve(){        return getInstance();    }}

当第一次加载Singleton时并不会初始化instance,只有在第一次调用Singleton的getInstance()方法是才会触发instance的初始化。因此,第一次调用getInstance()方法会导致虚拟机加载SingletonFactory类,这种方式不仅能够确保线程安全,也能够保证单例对象的唯一性,同事也延迟了单例的实例化,所以这是推荐使用的暂时完美的单例模式。

二、使用广泛的单例:Double Check Lock(DCL)单例模式

public class Singleton {    //私有构造方法,防止被实例化    private Singleton(){}    private static Singleton instance = null;    //获取实例    public static Singleton getInstance(){        if(instance == null){            synchronized(){                if(instance == null){                    instance = new Singleton();                }            }        }        return instance;    }    //如果对象被用于序列化,可以保证对象在序列化前后一致    public Object readResolve(){        return getInstance();    }}

DCL单例的亮点在getInstance()上,可以看到getInstance()方法中对instance进行了两次判空:第一层判断主要是为了避免不必要的同步,第二层判断则是为了在null的情况下创建实例。
我们来具体分析下。
假设线程A执行到instance = new Singleton()语句,这里看起来是语句代码,但实际上它并不是一个原子操作,这句代码最终会编译成多条汇编指令,它大致做了3个动作:
a.给Singleton实例分配内存;
b.调用Singleton()的构造函数,初始化成员字段;
c.将instance 对象指向分配的内存空间(instance 此时不为空)。

But……,由于Java编译器允许处理器乱序执行,以及JDK1.5之前JMM(Java Memory Model即Java内存模型)中Cache、寄存器到主内存回写顺序的规定,上面的b和c顺序是无法保证的。也就是说,执行顺序可能是a-b-c,也可能是a-c-b。如果是后者,并且在c执行完b未执行之前, 切换到B线程上,这时候instance 以为已经在A线程内执行过了c,instance 已经是非空了,所以,线程B直接取走instance ,再使用时就会报空,这就是DCL失效问题, 而且这种错误很难以追踪和重现,可能会导致bug长期潜伏在程序中,造成严重后果。

在JDK1.5之后,SUN官方已经注意到这个问题,调整了JVM,具体化了volatile关键字。因此,如果是JDK1.5或者1.5之后的版本,只需要将instance的定义改成private static volatile Singleton instance = null;就可以保证instance 对象每次都是从主内存中读取的,就可以使用DCL的写法来完成单例模式。
网上的资料有的说volatile或多或少会影响到性能,到考虑到程序的正确性,牺牲这点微不足道的性能还是值得的。

DCL方法实现单例模式的优点:既能够在需要时才开始实例化,又能够保证线程安全,且单例对象初始化后调用getInstance()不进行同步锁。
缺点:第一次加载时反应慢,也由于Java内存模型原因偶尔会失败。在高并发的环境下也有一定的缺陷,虽然发生的概率很小。这个问题被称为双重检查锁定(DCL)失效,在《Java并发编程实践》一书的最后讲到了这个问题,并指出这种“优化”是丑陋的,不赞成使用,建议使用例一的单例模式。

原创粉丝点击