单例模式深入分析

 

一.  单例模式简介

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

二. 单例模式实现

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

     代码1、懒汉模式

 
  1. package cn.edu.hit.Singleton;  
  2.   
  3. /** 
  4.  * Singleton的懒汉模式 
  5.  * @author yuanliming 
  6.  * @created 2009-9-2 
  7.  */  
  8. public class Singleton   
  9. {  
  10.     private static Singleton singleton;  
  11.       
  12.     private Singleton()  
  13.     {     
  14.     }  
  15.       
  16.     public static Singleton getInstance()  
  17.     {  
  18.         if(null == singleton)  
  19.         {  
  20.             singleton = new Singleton();  
  21.         }  
  22.         return singleton;  
  23.     }  
  24. }  

     代码2、饿汉模式

 
  1. package cn.edu.hit.Singleton;  
  2.   
  3. /** 
  4.  * Singleton的饿汉模式 
  5.  * @author yuanliming 
  6.  * @created 2009-9-2 
  7.  */  
  8. public class AnotherSingleton  
  9. {  
  10.     private static AnotherSingleton singleton = new AnotherSingleton();   
  11.       
  12.     private AnotherSingleton()  
  13.     {  
  14.     }  
  15.       
  16.     public static AnotherSingleton getInstance()  
  17.     {  
  18.         return singleton;  
  19.     }  
  20. }  

    代码3. 测试代码 

 
  1. package cn.edu.hit.Singleton;  
  2.   
  3. /** 
  4.  * Test Singleton 
  5.  * @author yuanliming 
  6.  * @created 2009-9-2 
  7.  */  
  8. public class SingletonTest  
  9. {  
  10.     public static void main(String[] args)  
  11.     {  
  12.         Singleton s1 = Singleton.getInstance();  
  13.         Singleton s2 = Singleton.getInstance();  
  14.           
  15.         System.out.println(s1 == s2);   //return true   
  16.           
  17.         AnotherSingleton s3 = AnotherSingleton.getInstance();  
  18.         AnotherSingleton s4 = AnotherSingleton.getInstance();  
  19.           
  20.         System.out.println(s3 == s4);   //return true   
  21.     }  
  22. }  

     两种实现模式的比较:
     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()方法就创建了2个Singleton对象,与单例模式的意图相违背。通过使用synchronized同步 getInstance() 方法从而在同一时间只允许一个线程执行代码。代码如下:

    代码4:

   

 
  1. package cn.edu.hit.Singleton;  
  2.   
  3. /** 
  4.  * Singleton的懒汉模式 
  5.  * @author yuanliming 
  6.  * @created 2009-9-2 
  7.  */  
  8. public class Singleton   
  9. {  
  10.     private static Singleton singleton;  
  11.       
  12.     private Singleton()  
  13.     {     
  14.     }  
  15.       
  16.     public static synchronized Singleton getInstance()  
  17.     {  
  18.         if(null == singleton)  
  19.         {  
  20.             singleton = new Singleton();  
  21.         }  
  22.         return singleton;  
  23.     }  
  24. }  

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

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

    代码5:

   

 
  1. package cn.edu.hit.Singleton;  
  2.   
  3. /** 
  4.  * Singleton的懒汉模式 
  5.  * @author yuanliming 
  6.  * @created 2009-9-2 
  7.  */  
  8. public class Singleton   
  9. {  
  10.     private static Singleton singleton;  
  11.       
  12.     private Singleton()  
  13.     {     
  14.     }  
  15.       
  16.     public static Singleton getInstance()  
  17.     {  
  18.         if(null == singleton)  
  19.         {  
  20.             synchronized (Singleton.class)  
  21.             {  
  22.                 singleton = new Singleton();  
  23.             }  
  24.         }  
  25.         return singleton;  
  26.     }  
  27. }  

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

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

    代码6:

   

 
  1. package cn.edu.hit.Singleton;  
  2.   
  3. /** 
  4.  * Singleton的懒汉模式 
  5.  * @author yuanliming 
  6.  * @created 2009-9-2 
  7.  */  
  8. public class Singleton   
  9. {  
  10.     private static Singleton singleton;  
  11.       
  12.     private Singleton()  
  13.     {     
  14.     }  
  15.       
  16.     public static Singleton getInstance()  
  17.     {  
  18.         if(null == singleton)  
  19.         {  
  20.             synchronized (Singleton.class)  
  21.             {  
  22.                 if(null == singleton)  
  23.                 {  
  24.                     singleton = new Singleton();  
  25.                 }  
  26.             }  
  27.         }  
  28.         return singleton;  
  29.     }  
  30. }  

    双重检查锁定在理论上能够保证代码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(尽管效率低下,但可以保证线程同步)。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值