现在的位置: 首页 > 综合 > 正文

单例模式深入分析

2013年04月29日 ⁄ 综合 ⁄ 共 4474字 ⁄ 字号 评论关闭

一.  单例模式简介

      单例(Singleton)模式是使用最广泛的设计模式。其思想意图是保证一个类只有一个实例,并且提供类对象的全程访问。单实例对象应用的范围很广:如GUI应用必须是单鼠标,MODEM的联接需要一条且只需要一条电话线,操作系统只能有一个窗口管理器,一台PC连一个键盘。使用全程对象能够保证方便地访问实例,但是不能保证只声明一个对象-也就是说除了一个全程实例外,仍然能创建相同类的本地实例。单实例模式通过类本身来管理其唯一实例,这种特性提供了问题的解决办法。唯一的实例是类的一个普通对象,但设计这个类时,让它只能创建一个实例并提供对此实例的全程访问。唯一实例类Singleton在静态成员函数中隐藏创建实例的操作。

二. 单例模式实现

     单例模式在java里有两个实现方式:1、懒汉模式; 2、饿汉模式。

     代码1、懒汉模式

     代码2、饿汉模式

    代码3. 测试代码 

     两种实现模式的比较:
     1、相同点:两种方式的构造函数都是私有的,对外的接口都是工厂方法。

    2、不同点:饿汉式是在类装载的时候直接得到该类的实例,可以说是前期绑定的;懒汉式是后期绑定的,类加载的时候uniSingleton是空的,在需要的时候才被创建且仅创建一次。饿汉式的速度快,效率高,但是耗费系统资源;懒汉式则相反。

    注意:懒汉式还存在一个问题,就是后期绑定不能确保对象只能被实例化一次。这就涉及到线程安全。

三. 单例模式的线程安全性探讨

       如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

       线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全。

       单例模式下最关心的就是这个线程安全的问题了。上面我们提到懒汉模式会有线程安全的问题。当引入多线程时,就必须通过同步来保护getInstance() 方法。如果不保护 getInstance() 方法,则可能返回 Singleton 对象的两个不同的实例。假设两个线程并发调用 getInstance() 方法并且按以下顺序执行调用:

       1.   线程 1 调用 getInstance() 方法并决定 instance //1 处为 null

      2.   线程 1 进入 if 代码块,但在执行 //2 处的代码行时被线程 2 预占。

      3.   线程 2 调用 getInstance() 方法并在 //1 处决定 instance null

      4.   线程 2 进入 if 代码块并创建一个新的 Singleton 对象并在 //2 处将变量 instance 分配给这个新对象。

      5.   线程 2 //3 处返回 Singleton 对象引用。

      6.   线程 2 被线程 1 预占。

      7.   线程 1 在它停止的地方启动,并执行 //2 代码行,这导致创建另一个 Singleton 对象。

      8.   线程 1 //3 处返回这个对象。

    这样,getInstance()方法就创建了2Singleton对象,与单例模式的意图相违背。通过使用synchronized同步 getInstance() 方法从而在同一时间只允许一个线程执行代码。代码如下:

    代码4

   

    此代码针对多线程访问 getInstance() 方法运行得很好。然而,分析这段代码,您会意识到只有在第一次调用方法时才需要同步。由于只有第一次调用执行了 //2 处的代码,而只有此行代码需要同步,因此就无需对后续调用使用同步。所有其他调用用于决定 instance 是非 null 的,并将其返回。多线程能够安全并发地执行除第一次调用外的所有调用。尽管如此,由于该方法是 synchronized 的,需要为该方法的每一次调用付出同步的代价,即使只有第一次调用需要同步。

因为代码4中只有//2需要同步,我们可以只将其包装到一个同步块中。得到的代码如下:

    代码5

   

    可是代码5出现了代码1同样的问题。当 instance null 时,两个线程可以并发地进入 if 语句内部。然后,一个线程进入 synchronized 块来初始化 instance,而另一个线程则被阻断。当第一个线程退出 synchronized 块时,等待着的线程进入并创建另一个 Singleton 对象。注意:当第二个线程进入 synchronized 块时,它并没有检查 instance 是否非 null

为了解决代码5出现的问题,我们对instance进行两次检查,即“双重检查锁定”。代码如下:

    代码6

   

    双重检查锁定在理论上能够保证代码6只创建一个Singleton对象。假设有下列事件序列:

    1. 线程 1 进入 getInstance() 方法。

      2. 由于 instance null,线程 1 //1 处进入 synchronized 块。

      3. 线程 1 被线程 2 预占。

      4. 线程 2 进入 getInstance() 方法。

      5. 由于 instance 仍旧为 null,线程 2 试图获取 //1 处的锁。然而,由于线程 1 持有该锁,线程 2 //1 处阻塞。

      6. 线程 2 被线程 1 预占。

      7. 线程 1 执行,由于在 //2 处实例仍旧为 null,线程 1 还创建一个 Singleton 对象并将其引用赋值给 instance

      8. 线程 1 退出 synchronized 块并从 getInstance() 方法返回实例。

      9. 线程 1 被线程 2 预占。

     10. 线程 2 获取 //1 处的锁并检查 instance 是否为 null

     11. 由于 instance 是非 null 的,并没有创建第二个 Singleton 对象,由线程 1 创建的对象被返回。

    看起来,双重检查锁定既解决了代码4的效率低下问题,又解决了代码5的线程安全性问题。但是它并不能保证它会在单处理器或多处理器计算机上顺利运行,根源在于 Java 平台内存模型。深入了解可以参考相关资料。

四.单例模式的选择

    无论以何种形式,都不应使用双重检查锁定,因为您不能保证它在任何 JVM 实现上都能顺利运行。

    如果只在单线程环境下运行,最好使用代码1

    如果涉及到多线程环境,最好使用代码2,也可以使用代码4(尽管效率低下,但可以保证线程同步)。

 

 

 

 

 

 

 

 

 

 

抱歉!评论已关闭.