单例模式之线程安全解析

面试的时候,常常会被问到这样一个问题:请您写出一个单例模式(Singleton Pattern)吧。 
    单例的目的是为了保证运行时Singleton类只有唯一的一个实例,最常用的地方比如拿到数据库的连接,Spring的中创建BeanFactory这些开销比较大的操作,而这些操作都是调用他们的方法来执行某个特定的动作。 
    很容易,顺手写一个《Java与模式》中的第一个例子: 
Java代码   收藏代码
  1. public final class Singleton {  
  2.     private static Singleton instance = new Singleton();  
  3.     private Singleton() {}  
  4.     public static Singleton getInstance() {  
  5.         return instance;  
  6.     }  
  7. }    

    这种写法就是所谓的饿汉式,每个对象在没有使用之前就已经初始化了。 
    问题来了,问题1:单例会带来什么问题?如果这个对象很大呢?没有使用这个对象之前,就把它加载到了内存中去是一种巨大的浪费。 
    针对这种情况,我们可以对以上的代码进行改进,使用一种新的设计思想——延迟加载(Lazy-load Singleton)。 
Java代码   收藏代码
  1. public final class Singleton{    
  2.     private static Singleton instance = null;  
  3.     private Singleton(){}    
  4.     public static Singleton getInstance(){    
  5.         if(instance == null){  
  6.             instance = new Singleton();  
  7.         }   
  8.         return instance;  
  9.     }    
  10. }   

    这种写法就是所谓的懒汉式。它使用了延迟加载来保证对象在没有使用之前,是不会进行初始化的。 
    通常这个时候面试官又会提问新的问题来刁难一下。他会问:这种写法线程安全吗?回答必然是:不安全。 
    这是因为在多个线程可能同时运行到第九行,判断instance为null,于是同时进行了初始化,出现创建多个实例的情况。 
    实际上使用什么样的单例实现取决于不同的生产环境,懒汉式适合于单线程程序,多线程情况下需要保护getInstance()方法,否则可能会产生多个Singleton对象的实例。    
    所以,这是面临的问题是如何使得这个代码线程安全?很简单,在getInstance()方法前面加一个synchronized关键字,锁定整个方法就OK了。 
Java代码   收藏代码
  1. public final class Singleton{     
  2.     private static Singleton instance=null;     
  3.     private Singleton(){}     
  4.     public static synchronized Singleton getInstance(){     
  5.         if(instance==null){     
  6.              instance=new Singleton();     
  7.          }     
  8.         return instance;     
  9.      }     
  10. }     

    写到这里,面试官可能仍然会狡猾的看了你一眼,继续刁难到:这个写法有没有什么性能问题呢?答案 肯定是有的! 同步的代价必然会一定程度的使程序的并发度降低。  
    锁定整个方法的是比较耗费资源的,代码中实际会产生多线程访问问题的只有
Java代码   收藏代码
  1. instance = new Singleton();  

    为了降低 synchronized 块性能方面的影响,把同步的粒度降低,只在初始化对象的时候进行同步,故只锁定初始化对象语句即可。 
Java代码   收藏代码
  1. public final Singleton getInstance(){        
  2.     if(instance == null){        
  3.          synchronize(this){           
  4.             instance =  new Singleton();             
  5.          }        
  6.      }        
  7.     return instance;     
  8. }      

    分析这种实现方式,两个线程可以并发地进入第一次判断instance是否为空的if 语句内部,第一个线程执行new操作,第二个线程阻断,当第一个线程执行完毕之后,第二个线程没有进行判断就直接进行new操作,所以这样做也并不是安全的。 
    为了避免第二次进入synchronized块没有进行非空判断的情况发生,添加第二次条件判断,即一种新的设计思想—— 双重检查锁(Double-Checked Lock) 。 
Java代码   收藏代码
  1. public final class Singleton{     
  2.     private static Singleton instance=null;     
  3.     private Singleton(){}     
  4.     public static Singleton getInstance(){        
  5.       if(instance == null){        
  6.          synchronize(this){        
  7.            if(instance == null){        
  8.                instance =  new Singleton();         
  9.             }        
  10.          }        
  11.       }        
  12.       return instance;     
  13.     }       
  14. }     

    这种写法使得只有在加载新的对象进行同步,在加载完了之后,其他线程在第5行就可以判断跳过锁的的代价直接到第12行代码了。做到很好的并发度。 
    至此,上面的写法一方面实现了Lazy-Load,另一个方面也做到了并发度很好的线程安全,一切看上很完美。 
    但是二次检查自身会存在比较隐蔽的问题,查了Peter Haggar在DeveloperWorks上的一篇文章,对二次检查的解释非常的详细: 
“双重检查锁定背后的理论是完美的。不幸地是,现实完全不同。双重检查锁定的问题是:并不能保证它会在单处理器或多处理器计算机上顺利运行。双重检查锁定失败的问题并不归咎于 JVM 中的实现 bug,而是归咎于 Java 平台内存模型。内存模型允许所谓的“无序写入”,这也是这些习语失败的一个主要原因。” 
    使用二次检查的方法也不是完全安全的,原因是 java 平台内存模型中允许所谓的“无序写入”会导致二次检查失败,所以使用二次检查的想法也行不通了。 
    Peter Haggar在最后提出这样的观点:“无论以何种形式,都不应使用双重检查锁定,因为您不能保证它在任何 JVM 实现上都能顺利运行。” 
    问题在哪里? 
    假设线程A执行到了第5行,它判断对象为空,于是线程A执行到第8行去初始化这个对象,但初始化是需要耗费时间的,但是这个对象的地址其实已经存在了。此时线程B也执行到了第5行,它判断不为空,于是直接跳到12行得到了这个对象。但是,这个对象还没有被完整的初始化!得到一个没有初始化完全的对象有什么用!! 
    关于这个Double-Checked Lock的讨论有很多,目前公认这是一个Anti-Pattern,不推荐使用! 
    那么有没有什么更好的写法呢? 
    有!这里又要提出一种新的模式—— Initialization on Demand Holder . 这种方法使用内部类来做到延迟加载对象,在初始化这个内部类的时候,JLS(Java Language Sepcification)会保证这个类的线程安全。这种写法最大的美在于,完全使用了Java虚拟机的机制进行同步保证,没有一个同步的关键字。 
Java代码   收藏代码
  1. public class ResourceFactory{  
  2.     private static class ResourceHolder{  
  3.         public static Resource resource = new Resource();        
  4.     }       
  5.     public static Resource getResource() {  
  6.         return ResourceFactory.ResourceHolder.resource;        
  7.     }  
  8. }     

    上面的方式是值得借鉴的,在ResourceFactory中加入了一个私有静态内部类ResourceHolder ,对外提供的接口是 getResource()方法,也就是只有在ResourceFactory .getResource()的时候,Resource对象才会被创建, 
    这种写法的巧妙之处在于ResourceFactory 在使用的时候ResourceHolder 会被初始化,但是ResourceHolder 里面的resource并没有被创建, 
    这里隐含了一个是static关键字的用法,使用static关键字修饰的变量只有在第一次使用的时候才会被初始化,而且一个类里面static的成员变量只会有一份,这样就保证了无论多少个线程同时访问,所拿到的Resource对象都是同一个。 
    值得注意的是,饿汉式的实现方式虽然貌似开销比较大,但是不会出现线程安全的问题,也是解决线程安全的单例实现的有效方式。 
    所以本文提出的第一个例子(也是《Java与模式》中的例子),也是使用单例模式的有效方法之一。这种方式没有使用同步,并且确保了调用static getInstance()方法时才创建Singleton的引用(static 的成员变量在一个类中只有一份)。 
    附: 
    饿汉式单例类可以在Java语言实现,但不易在C++内实现,因为静态初始化在C++里没有固定的顺序,因而静态的instance变量的初始化与类的加载顺序没有保证,可能会出问题。这就是为什么GoF在提出单例类的概念时,举的例子是懒汉式的。他们的书影响之大,以致Java语言中单例类的例子也大多是懒汉式的。实际上,本书认为饿汉式单例类更符合Java语言本身的特点。 

                                                         ——《Java与模式》作者 


参考资料: 
Double-Checked Lock:http://en.wikipedia.org/wiki/Double-checked_locking 
Initialzation on Demand Holder: http://en.wikipedia.org/wiki/Initialization_on_demand_holder_idiom 
线程安全的单例模式http://blog.sina.com.cn/s/blog_75247c770100yxpb.html 
线程安全的单例模式http://hi.baidu.com/snbrskt/item/e8b12c16bc62b407d0d66d03 
双重检查锁定及单例模式http://www.ibm.com/developerworks/cn/java/j-dcl.html#author 
Lazy Loading Singletonshttp://blog.crazybob.org/2007/01/lazy-loading-singletons.html

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值