单例模式的七种写法, 面试题:线程安全的单例模式


http://cantellow.iteye.com/blog/838473


http://meizhi.iteye.com/blog/537563


转载请注明出处:http://cantellow.iteye.com/blog/838473

 

第一种(懒汉,线程不安全):

 

Java代码   收藏代码
  1. public class Singleton {  
  2.     private static Singleton instance;  
  3.     private Singleton (){}  
  4.   
  5.     public static Singleton getInstance() {  
  6.     if (instance == null) {  
  7.         instance = new Singleton();  
  8.     }  
  9.     return instance;  
  10.     }  
  11. }  
 

 这种写法lazy loading很明显,但是致命的是在多线程不能正常工作。

第二种(懒汉,线程安全):

 

Java代码   收藏代码
  1. public class Singleton {  
  2.     private static Singleton instance;  
  3.     private Singleton (){}  
  4.     public static synchronized Singleton getInstance() {  
  5.     if (instance == null) {  
  6.         instance = new Singleton();  
  7.     }  
  8.     return instance;  
  9.     }  
  10. }  
 

 这种写法能够在多线程中很好的工作,而且看起来它也具备很好的lazy loading,但是,遗憾的是,效率很低,99%情况下不需要同步。

第三种(饿汉):

 

Java代码   收藏代码
  1. public class Singleton {  
  2.     private static Singleton instance = new Singleton();  
  3.     private Singleton (){}  
  4.     public static Singleton getInstance() {  
  5.     return instance;  
  6.     }  
  7. }  
 

 这种方式基于classloder机制避免了多线程的同步问题,不过,instance在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用getInstance方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance显然没有达到lazy loading的效果。

第四种(饿汉,变种):

 

Java代码   收藏代码
  1. public class Singleton {  
  2.     private Singleton instance = null;  
  3.     static {  
  4.     instance = new Singleton();  
  5.     }  
  6.     private Singleton (){}  
  7.     public static Singleton getInstance() {  
  8.     return this.instance;  
  9.     }  
  10. }  
 

 表面上看起来差别挺大,其实更第三种方式差不多,都是在类初始化即实例化instance。

第五种(静态内部类):

 

Java代码   收藏代码
  1. public class Singleton {  
  2.     private static class SingletonHolder {  
  3.     private static final Singleton INSTANCE = new Singleton();  
  4.     }  
  5.     private Singleton (){}  
  6.     public static final Singleton getInstance() {  
  7.     return SingletonHolder.INSTANCE;  
  8.     }  
  9. }  
 

这种方式同样利用了classloder的机制来保证初始化instance时只有一个线程,它跟第三种和第四种方式不同的是(很细微的差别):第三种和第四种方式是只要Singleton类被装载了,那么instance就会被实例化(没有达到lazy loading效果),而这种方式是Singleton类被装载了,instance不一定被初始化。因为SingletonHolder类没有被主动使用,只有显示通过调用getInstance方法时,才会显示装载SingletonHolder类,从而实例化instance。想象一下,如果实例化instance很消耗资源,我想让他延迟加载,另外一方面,我不希望在Singleton类加载时就实例化,因为我不能确保Singleton类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化instance显然是不合适的。这个时候,这种方式相比第三和第四种方式就显得很合理。

第六种(枚举):

 

Java代码   收藏代码
  1. public enum Singleton {  
  2.     INSTANCE;  
  3.     public void whateverMethod() {  
  4.     }  
  5. }  

 

 这种方式是Effective Java作者Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象,可谓是很坚强的壁垒啊,不过,个人认为由于1.5中才加入enum特性,用这种方式写不免让人感觉生疏,在实际工作中,我也很少看见有人这么写过。

第七种(双重校验锁):

Java代码   收藏代码
  1. public class Singleton {  
  2.     private volatile static Singleton singleton;  
  3.     private Singleton (){}  
  4.     public static Singleton getSingleton() {  
  5.     if (singleton == null) {  
  6.         synchronized (Singleton.class) {  
  7.         if (singleton == null) {  
  8.             singleton = new Singleton();  
  9.         }  
  10.         }  
  11.     }  
  12.     return singleton;  
  13.     }  
  14. }  
 

 这个是第二种方式的升级版,俗称双重检查锁定,详细介绍请查看:http://www.ibm.com/developerworks/cn/java/j-dcl.html

在JDK1.5之后,双重检查锁定才能够正常达到单例效果。

 

总结

有两个问题需要注意:

1.如果单例由不同的类装载器装入,那便有可能存在多个单例类的实例。假定不是远端存取,例如一些servlet容器对每个servlet使用完全不同的类装载器,这样的话如果有两个servlet访问一个单例类,它们就都会有各自的实例。

2.如果Singleton实现了java.io.Serializable接口,那么这个类的实例就可能被序列化和复原。不管怎样,如果你序列化一个单例类的对象,接下来复原多个那个对象,那你就会有多个单例类的实例。

对第一个问题修复的办法是:

 

Java代码   收藏代码
  1. private static Class getClass(String classname)      
  2.                                          throws ClassNotFoundException {     
  3.       ClassLoader classLoader = Thread.currentThread().getContextClassLoader();     
  4.       
  5.       if(classLoader == null)     
  6.          classLoader = Singleton.class.getClassLoader();     
  7.       
  8.       return (classLoader.loadClass(classname));     
  9.    }     
  10. }  

 对第二个问题修复的办法是:

 

Java代码   收藏代码
  1. public class Singleton implements java.io.Serializable {     
  2.    public static Singleton INSTANCE = new Singleton();     
  3.       
  4.    protected Singleton() {     
  5.         
  6.    }     
  7.    private Object readResolve() {     
  8.             return INSTANCE;     
  9.       }    
  10. }   
 

对我来说,我比较喜欢第三种和第五种方式,简单易懂,而且在JVM层实现了线程安全(如果不是多个类加载器环境),一般的情况下,我会使用第三种方式,只有在要明确实现lazy loading效果时才会使用第五种方式,另外,如果涉及到反序列化创建对象时我会试着使用枚举的方式来实现单例,不过,我一直会保证我的程序是线程安全的,而且我永远不会使用第一种和第二种方式,如果有其他特殊的需求,我可能会使用第七种方式,毕竟,JDK1.5已经没有双重检查锁定的问题了。

========================================================================

 superheizai同学总结的很到位:

 

不过一般来说,第一种不算单例,第四种和第三种就是一种,如果算的话,第五种也可以分开写了。所以说,一般单例都是五种写法。懒汉,恶汉,双重校验锁,枚举和静态内部类。

我很高兴有这样的读者,一起共勉




=======================================================================



面试被问到一个线程安全的单例模式问题,想拿出来讨论一下,

我通常会使用的这样的写法来实现单例:

 

Java代码   收藏代码
  1. public class Singleton {  
  2.       
  3.     private Singleton() {}  
  4.     private static Singleton instance = null;  
  5.   
  6.     public static Singleton getInstance() {  
  7.         if(instance == null) {  
  8.             instance = new Singleton();  
  9.         }  
  10.         return instance;  
  11.     }  
  12. }  

 

单例的目的是为了保证运行时Singleton类只有唯一的一个实例,最常用的地方比如拿到数据库的连接,spring的中创建BeanFactory这些开销比较大的操作,而这些操作都是调用他们的方法来执行某个特定的动作。


面试官的问题是:单例会带来什么问题?


我第一反映就是如果多个线程同时调用这个实例,会有线程安全的问题,当时就这么说了,然后他问:“怎么实现一个线程安全的单例模式呢?”


这个问题我没有回答上来,当时脑子里闪了一下如果用synchronized来锁定可能会有一些问题,至于是什么问题没有想明白,就选择没有回答。


这里请问各位高手,

1、如果不执行修改对象的操作的情况下,单单执行一个读取操作,还有没有进行同步的必要?

2、保证单例的线程安全使用synchronized会产生什么样的问题?

3、不使用synchronized,有什么方式来保证线程安全?

4、假如下次再面试遇到这种情形,用什么方式回答会使面试官感到比较满意?

 

 

--------------------------------------------------------------------------------------------------------------------------------------------------------------

 

感谢大家的讨论与支持,总结一下:

 

实际上使用什么样的单例实现取决于不同的生产环境,懒汉式也就是我在上面举得那个例子,这种方式适合于单线程程序,多线程情况下需要保护getInstance()方法,否则可能会产生多个Singleton对象的实例。

 

在此基础上确保getInstance()方法一次只能被一个线程调用就需要在getInstance()方法之前加上 synchronized 关键字,锁定整个方法,

 

Java代码   收藏代码
  1. public 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. }   
 

 

但很多时候我们通常会认为锁定整个方法的是比较耗费资源的,代码中实际会产生多线程访问问题的只有 instance = new Singleton(); 这一句,

为了降低 synchronized 块性能方面的影响,只锁定instance = new Singleton(); 这一句,“weishuang”回帖中使用的就是这种方式:

 

 

Java代码   收藏代码
  1. public class Singleton{   
  2.     private static Singleton instance=null;   
  3.     private Singleton(){}   
  4.     public static Singleton getInstance(){   
  5.         if(instance==null){   
  6.             synchronized(Singleton.class){   
  7.                 instance=new Singleton();   
  8.             }   
  9.         }   
  10.         return instance;   
  11.     }   
  12. }   
 

 

分析这种实现方式,两个线程可以并发地进入第一次判断instance是否为空的if 语句内部,第一个线程执行new操作,第二个线程阻断,当第一个线程执行完毕之后,第二个线程没有进行判断就直接进行new操作,所以这样做也并不是安全的。

 

为了避免第二次进入synchronized块没有进行非空判断的情况发生,添加第二次条件判断,就像“tomorrow009”在帖子中回复的示例一样

 

Java代码   收藏代码
  1. public static Singleton getInstance(){     
  2.     if(instance == null){     
  3.         synchronize{     
  4.            if(instance == null){     
  5.               instance =  new Singleton();      
  6.            }     
  7.         }     
  8.     }     
  9.     return instance;  
  10. }    

 

 

这样就产生了二次检查,但是二次检查自身会存在比较隐蔽的问题,查了Peter HaggarDeveloperWorks上的一篇文章,对二次检查的解释非常的详细:

“双重检查锁定背后的理论是完美的。不幸地是,现实完全不同。双重检查锁定的问题是:并不能保证它会在单处理器或多处理器计算机上顺利运行。双重检查锁定失败的问题并不归咎于 JVM 中的实现 bug,而是归咎于 Java 平台内存模型。内存模型允许所谓的“无序写入”,这也是这些习语失败的一个主要原因。”

 

其实找到这篇文章之后,我的问题基本上就已经可以解决了,但是看到回帖的同学们也有一些和我一样的问题,还想把这个问题继续梳理一遍。

 

使用二次检查的方法也不是完全安全的,原因是 Java 平台内存模型中允许所谓的“无序写入”会导致二次检查失败,所以使用二次检查的想法也行不通了。

 

Peter Haggar在最后提出这样的观点:“无论以何种形式,都不应使用双重检查锁定,因为您不能保证它在任何 JVM 实现上都能顺利运行。”

 

"netrice"在回复中提到了使用“java5以后的volatile关键字”,用volatile关键字来声明变量,声明成 volatile 的变量被认为是顺序一致的,即,不是重新排序的。但是volatile关键字的特性并不适用于这篇帖子所讨论的问题关键。

 

通过上面的分析,可以看到使用懒汉式的lazy方式实现单例弯弯绕太多,在单线程编程的情况下懒汉式单例实现是没有任何问题的,如果在多线程的情况下,我们需要比较小心,对getInstances()方法加上synchronized关键字,这样虽然可能有一些性能上的牺牲,但是更加的安全。绕了这么大的一个弯,又回来了:

 

Java代码   收藏代码
  1. /* 安全的方式 1 */  
  2. public class Singleton{   
  3.     private static Singleton instance=null;   
  4.     private Singleton(){}   
  5.     public static synchronized Singleton getInstance(){   
  6.         if(instance==null){   
  7.             instance=new Singleton();   
  8.         }   
  9.         return instance;   
  10.     }   
  11. }   
  

Peter Haggar提到的另外一种实现方式是这样的,放弃使用 synchronized 关键字,而使用 static 关键字:

 

Java代码   收藏代码
  1. /* 安全的方式 2 */  
  2. public class Singleton {  
  3.   
  4.   private static Singleton instance = new Singleton();  
  5.   
  6.   private Singleton() {}  
  7.   
  8.   public static Singleton getInstance() {  
  9.     return instance;  
  10.   }  
  11.   
  12. }  

 

这种方式没有使用同步,并且确保了调用static getInstance()方法时才创建Singleton的引用(static 的成员变量在一个类中只有一份)。

 

还有“keshin”提到的方式则更加灵巧,没有使用同步但保证了只有一个实例,还同时具有了Lazy的特性(出自Lazy Loading Singletons

 

Java代码   收藏代码
  1. /* 安全的方式 3 */  
  2. public class ResourceFactory {     
  3.     private static class ResourceHolder {     
  4.         public static Resource resource = new Resource();     
  5.     }     
  6.     
  7.     public static Resource getResource() {     
  8.         return ResourceFactory.ResourceHolder.resource;     
  9.     }     
  10.     
  11.     static class Resource {     
  12.     }     
  13. }    
  

上面的方式是值得借鉴的,在ResourceFactory中加入了一个私有静态内部类ResourceHolder ,对外提供的接口是 getResource()方法,也就是只有在ResourceFactory .getResource()的时候,Resource对象才会被创建,

 

这种写法的巧妙之处在于ResourceFactory 在使用的时候ResourceHolder 会被初始化,但是ResourceHolder 里面的resource并没有被创建,

 

这里隐含了一个是static关键字的用法,使用static关键字修饰的变量只有在第一次使用的时候才会被初始化,而且一个类里面static的成员变量只会有一份,这样就保证了无论多少个线程同时访问,所拿到的Resource对象都是同一个。


饿汉式的实现方式虽然貌似开销比较大,但是不会出现线程安全的问题,也是解决线程安全的单例实现的有效方式。

 

至于ThreadLocal,我认为还是应该由使用场景来决定。

 

在《Java与模式》中,作者提出:“饿汉式单例类可以在Java语言实现,但不易在C++内实现,因为静态初始化在C++里没有固定的顺序,因而静态的instance变量的初始化与类的加载顺序没有保证,可能会出问题。这就是为什么GoF在提出单例类的概念时,举的例子是懒汉式的。他们的书影响之大,以致Java语言中单例类的例子也大多是懒汉式的。实际上,本书认为饿汉式单例类更符合Java语言本身的特点。”

 

由此可见在应用设计模式的同时,分析具体的使用场景来选择合适的实现方式是非常必要的。

 

寻找问题解决过程中找的一些参考资料:

锁定老贴子 主题:【转】单例模式完全剖析

双重检查锁定及单例模式

Lazy Loading Singletons

 

因为在精华帖中没有找到很流畅解释这个问题的内容才发了这个帖子,还是很不幸的被评为了新手帖,但如果下次有面试官问有关线程安全的单例模式问题,我想我知道该怎么回答了


  • 8
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值