`
fxly0401
  • 浏览: 147977 次
  • 性别: 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 单例模式的介绍以及解析

    线程安全是单例模式中的一个重要话题,尤其是在多线程环境下。如果不正确地实现单例模式,可能会导致多个线程同时创建类的实例,违反了单例模式的设计初衷。为了解决这个问题,我们可以采用以下几种策略: 1. **...

    一个C#版单例模式的xml解析类

    总结起来,这个C#版的单例模式XML解析类结合了单例设计模式和XML处理技术,为C#应用程序提供了一种高效且线程安全的方式来解析XML数据。使用`SingletonXmlParser`,开发人员可以在不担心实例化多个解析器的情况下,...

    设计模式——单例模式(懒汉模式)

    枚举的单例模式简单易懂,且天然线程安全,不会受到反射和序列化攻击的影响,是推荐的单例实现方式之一。 总的来说,单例模式是一种常见的设计模式,懒汉式单例模式则是其中一种实现策略,它的主要特点是延迟加载和...

    单例模式单例模式单例模式

    - **并发问题**:在多线程环境中,单例模式可能会导致线程安全问题,需要额外处理同步机制。 #### 五、实例解析 在给定的内容中提到的`Martin`类就是一个典型的单例模式实现案例。它通过将构造器私有化以及提供一...

    单例设计模式源码和案例解析

    在Java编程中,单例模式的应用广泛,例如控制资源的共享、管理配置信息等。本篇将深入解析单例设计模式的四种实现方式,并通过具体的案例进行详细分析。 一、懒汉式(线程不安全) 懒汉式单例的特点是在类被加载时...

    java设计模式之单例模式详解

    Java设计模式之单例模式详解 一、单例模式概览 单例模式(Singleton Pattern)是面向对象设计模式中的一种,属于创建型模式。它确保一个类仅有一个实例,并提供一个全局访问点来访问该实例。单例模式的核心在于控制...

    设计模式——策略模式 & 单例模式

    策略模式和单例模式是软件设计中两种非常重要的设计模式,它们在实际开发中有着广泛的应用。在这篇文章中,我们将深入探讨这两种模式的核心概念、实现方式以及如何在实际项目中运用。 策略模式是一种行为设计模式,...

    使用Java单例模式实现一个简单的日志记录器.txt

    通过定义静态变量、私有构造函数以及线程安全的实例获取方法,实现了日志记录器的单例模式。此外,还提供了写入日志的方法,便于实际应用中的日志管理。这种方法简单有效,适合初学者理解和实践。

    单例模式在实现飞机大战的源码.rar

    《深入解析:单例模式在实现“飞机大战”游戏中的应用》 在编程领域,设计模式是一种被广泛接受和使用的解决常见问题的方案,其中单例模式尤其受到开发者们的青睐。在C#编程中,单例模式是创建型设计模式之一,它...

    单例模式分享-吴昌良

    2. **线程安全问题**:在多线程环境中如何确保单例模式的线程安全。 3. **单例模式的优缺点**:讨论单例模式的优点与潜在的问题。 #### 七、总结 通过以上内容的学习,我们可以看到单例模式是一种非常实用的设计...

    经典工厂单例模式典范

    在软件设计模式的世界里,"工厂单例模式典范"是一个重要的概念,它结合了工厂模式和单例模式的优势,以解决特定的编程问题。工厂模式是一种创建型设计模式,而单例模式则是一种结构型设计模式。这两种模式的结合使得...

    java模式设计之单例模式

    【单例模式详解】 单例模式是设计模式中的一种,它的核心...以上是对单例模式的详细解析,它在Java编程中有着广泛的应用,特别是在需要全局唯一对象的场景中。理解并合理运用单例模式,可以提高代码的可维护性和性能。

    JAVA设计模式中的单例模式

    ### JAVA设计模式中的单例模式解析 在软件工程与编程领域,设计模式是解决特定问题的一套被广泛接受的解决方案。其中,单例模式(Singleton Pattern)是一种常用的创建型模式,其核心在于确保一个类只有一个实例,...

    c#单例模式示例

    在C#中,单例模式可以通过多种方式实现,如Eager Initialization(急切初始化)、Lazy Initialization(懒惰初始化)以及线程安全的懒惰加载等。本文将重点介绍C#中两种不同的单例模式实现方法:基于嵌套类的完全...

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

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

    常用设计模式特点比如工厂模式、单例模式等等

    在C++中,实现单例模式需要注意线程安全,尤其是在多线程环境下。 除了这两个模式,还有其他设计模式也值得了解: - "组件协作模式":如策略模式、观察者模式,它们帮助对象之间协同工作,实现灵活的行为调整。 - ...

    java单例模式开发的7种写法

    ### Java单例模式开发的七种写法 #### 概述 单例模式是一种常用的软件设计模式,其目的是确保一个类仅有一个实例,并提供一个全局访问点。在Java编程语言中,实现单例模式的方法有很多种,不同的实现方式具有不同的...

    Java设计模式单例模式(Singleton)用法解析

    Java设计模式单例模式(Singleton)用法解析 java设计模式单例模式(Singleton)是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局反访问点。单例模式是创建型模式。单例模式在生活中应用也很广泛,...

    qt 单例模式,模板

    在多线程环境中,单例模式确保对共享资源的唯一访问,避免并发问题。在Qt中,我们通常使用单例来管理数据库连接、配置文件或系统设置等。 3. **Singleton类**:在提供的描述中提到了`Singleton.h`和`...

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

    单例模式是软件设计模式中的一种...而枚举单例模式虽然简单且线程安全,但在某些场景下可能会显得过于冗余。理解并熟练运用这些单例模式,能帮助开发者更好地管理和优化Android应用中的对象实例,提高代码质量和性能。

Global site tag (gtag.js) - Google Analytics