`
fxly0401
  • 浏览: 143653 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

单例模式之线程安全解析

阅读更多
    本文综合网上资料以及代码时间,对要求延迟加载和线程安全的单例模式做了如下分析。
    自励共勉。

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

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

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

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

    为了降低 synchronized 块性能方面的影响,把同步的粒度降低,只在初始化对象的时候进行同步,故只锁定初始化对象语句即可。
public final Singleton getInstance(){      
    if(instance == null){      
         synchronize(this){         
            instance =  new Singleton();           
         }      
     }      
    return instance;   
}    

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

    这种写法使得只有在加载新的对象进行同步,在加载完了之后,其他线程在第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虚拟机的机制进行同步保证,没有一个同步的关键字。
public class ResourceFactory{
    private static class ResourceHolder{
        public static Resource resource = new Resource();      
    }     
    public static Resource getResource() {
        return ResourceFactory.ResourceHolder.resource;      
    }
}   

    上面的方式是值得借鉴的,在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
分享到:
评论
2 楼 august_000 2012-10-17  
很有道理,我已经亲自测试过了:
public class A {

private A(){
	System.out.println("init a....");
}
private static class ResourceHolder{   
	private static A a =new A();      
}  
public static A getInst(){
	System.out.println("getInst...");
	A a = ResourceHolder.a;
	return a;
}
}


结果:
getInst...
init a....
1 楼 Chris_bing 2012-10-17  
一个单例有这么多名堂,最后那个内部类的解决方案很有创意啊,受教了!

相关推荐

    Singleton 单例模式的介绍以及解析

    单例模式 Singleton 单例模式线程安全问题和拓展

    实验12 单例模式与枚举.doc

    本专栏主要为Java程序设计(基础)实验报告和Java程序设计(进阶)...进阶篇有反射、泛型、注解、网络编程、多线程、序列化、数据库、Servlet、JSP、XML解析、单例模式与枚举。本专栏主要为Java入门者提供实验参考。

    Android设计模式之单例模式解析

    今天我们要讲的是单例模式 定义 确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例 使用场景 确保某个类有且只有一个对象的场景,避免产生多个对象消耗过多的资源 某个类型的对象只应该有一个 ...

    Qt 串口通信,线程,单例。解析浮点数据,3字节的数据。

    串口管理类放到线程中,这样不会影响界面的流程,并且串口管理类用到了单例的设计模式,这个平时做项目积攒下了的经验,并且对3字节的数据进行了解析。用到了联合体,更方便的将16进制数转换成浮点数,发送数据的...

    OBJECTIVE-C编程之道 IOS设计模式解析电子书+源代码

    C中实现单例模式7.4 子类化Singleton7.5 线程安全7.6 在Cocoa Touch框架中使用单例模式7.6.1 使用UIApplication类7.6.2 使用UIAccelerometer类7.6.3 使用NSFileManager类7.7 总结第三部分 接口适配第8章 适配器8.1 ...

    Java实验6多线程.doc

    本专栏主要为Java程序设计(基础)实验报告和Java程序设计(进阶)...进阶篇有反射、泛型、注解、网络编程、多线程、序列化、数据库、Servlet、JSP、XML解析、单例模式与枚举。本专栏主要为Java入门者提供实验参考。

    Java并发编程原理与实战

    单例问题与线程安全性深入解析.mp4 理解自旋锁,死锁与重入锁.mp4 深入理解volatile原理与使用.mp4 JDK5提供的原子类的操作以及实现原理.mp4 Lock接口认识与使用.mp4 手动实现一个可重入锁.mp4 ...

    java设计模式

    28.4.1 线程安全的问题 28.4.2 性能平衡 28.5 最佳实践 第29章 桥梁模式 29.1 我有一个梦想…… 29.2 桥梁模式的定义 29.3 桥梁模式的应用 29.3.1 桥梁模式的优点 29.3.2 桥梁模式的应用 29.3.3 桥梁模式的注意事项 ...

    实验11 XML解析.doc

    本专栏主要为Java程序设计(基础)实验报告和Java程序设计(进阶)...进阶篇有反射、泛型、注解、网络编程、多线程、序列化、数据库、Servlet、JSP、XML解析、单例模式与枚举。本专栏主要为Java入门者提供实验参考。

    Java实例高难度面试题及解析 - 展现你的编程实力!

    您将了解如何正确创建对象实例、访问实例的成员变量和方法、实现对象的拷贝(包括浅拷贝和深拷贝)、判断对象相等性、管理对象的生命周期、实现线程安全的单例模式等。此外,我们还探讨了对象的哈希码、重写equals()...

    实验9 Servlet.doc

    本专栏主要为Java程序设计(基础)实验报告和Java程序设计(进阶)...进阶篇有反射、泛型、注解、网络编程、多线程、序列化、数据库、Servlet、JSP、XML解析、单例模式与枚举。本专栏主要为Java入门者提供实验参考。

    实验9 Java输入输出流.doc

    本专栏主要为Java程序设计(基础)实验报告和Java程序设计(进阶)...进阶篇有反射、泛型、注解、网络编程、多线程、序列化、数据库、Servlet、JSP、XML解析、单例模式与枚举。本专栏主要为Java入门者提供实验参考。

    Java实验7 序列化.doc

    本专栏主要为Java程序设计(基础)实验报告和Java程序设计(进阶)...进阶篇有反射、泛型、注解、网络编程、多线程、序列化、数据库、Servlet、JSP、XML解析、单例模式与枚举。本专栏主要为Java入门者提供实验参考。

    Java实验8 数据库.doc

    本专栏主要为Java程序设计(基础)实验报告和Java程序设计(进阶)...进阶篇有反射、泛型、注解、网络编程、多线程、序列化、数据库、Servlet、JSP、XML解析、单例模式与枚举。本专栏主要为Java入门者提供实验参考。

    实验10 JSP.doc

    本专栏主要为Java程序设计(基础)实验报告和Java程序设计(进阶)...进阶篇有反射、泛型、注解、网络编程、多线程、序列化、数据库、Servlet、JSP、XML解析、单例模式与枚举。本专栏主要为Java入门者提供实验参考。

    实验5 网络编程.doc

    本专栏主要为Java程序设计(基础)实验报告和Java程序设计(进阶)...进阶篇有反射、泛型、注解、网络编程、多线程、序列化、数据库、Servlet、JSP、XML解析、单例模式与枚举。本专栏主要为Java入门者提供实验参考。

    Java实验2 反射.doc

    本专栏主要为Java程序设计(基础)实验报告和Java程序设计(进阶)...进阶篇有反射、泛型、注解、网络编程、多线程、序列化、数据库、Servlet、JSP、XML解析、单例模式与枚举。本专栏主要为Java入门者提供实验参考。

    Java实验3泛型.doc

    本专栏主要为Java程序设计(基础)实验报告和Java程序设计(进阶)...进阶篇有反射、泛型、注解、网络编程、多线程、序列化、数据库、Servlet、JSP、XML解析、单例模式与枚举。本专栏主要为Java入门者提供实验参考。

    Java实验4 注解.doc

    本专栏主要为Java程序设计(基础)实验报告和Java程序设计(进阶)...进阶篇有反射、泛型、注解、网络编程、多线程、序列化、数据库、Servlet、JSP、XML解析、单例模式与枚举。本专栏主要为Java入门者提供实验参考。

    Java实验1代码.doc

    本专栏主要为Java程序设计(基础)实验报告和Java程序设计(进阶)...进阶篇有反射、泛型、注解、网络编程、多线程、序列化、数据库、Servlet、JSP、XML解析、单例模式与枚举。本专栏主要为Java入门者提供实验参考。

Global site tag (gtag.js) - Google Analytics