`
kidneyball
  • 浏览: 329004 次
  • 性别: Icon_minigender_1
  • 来自: 南太平洋
社区版块
存档分类
最新评论

关于单例模式的DoubleCheckLock同步的思考

阅读更多
在javaeye上看到很多朋友都提出单例模式的一些变种实现,比如加入了即时加载和DoubleCheckLock机制,来提高并发性能。但事实上这些机制真的必要吗?

目前公认影响单例性能的要素有两个:一是实例构造时间开销,一是获取单例实例的同步阻塞开销。

我的理解是,并发相对与同步阻塞的优势,在于当两条线程中的一条在执行时间开销较大的操作,而另一条线程无须执行该操作,则并发执行保证了开销小的线程不需等待开销大的,能正常执行完毕。然而,如果所有线程都只执行一些基本的操作,例如“返回结果”,“变量赋值”,“判断跳转”等,是否并发执行对性能并没有实质性的提升。差别只在于到底在jdk层面在同步块上排队,还是在cpu层面在时间片分配上排队。

对于一个基本的单例模式,在每个jvm中肯定只会发生一次单例构造。同步机制虽然保护的是构造过程,但99%时间锁的是返回结果过程。而对于这种基本操作,有锁和无锁差别真的是这么大(据说有100倍的差别)吗?

网上常见的单例实现有以下几种:

1. 延迟加载的基本实现
public class Singleton {   

    private static Singleton instance = null;   

    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

2. 即时加载的基本实现
public class Singleton {   

    private static Singleton instance = new Singleton();   

    public static Singleton getInstance() {
        return instance;
    }
}

3. DoubleCheck同步延迟加载
public class Singleton {   

    private static Singleton instance = null;   

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}


举个极端的量化例子,假如有一个单例对象被500条线程使用。单例的构造器中由于使用了一些耗时的资源,执行一次约为1s。但如果单例实例已经创建好,在getInstance()中直接return该实例,耗时为0.1ms。这500条线程中的其中200条会每隔10分钟(以机器内时钟为准)同时访问这个单例对象。

另外,先假定并发操作对于一些基本的操作,例如方法返回,赋值,判断跳转等不会带来性能上的大幅提升(这与JDK的实现有关)。换句话说,200条线程中“返回实例”动作,无论是否在同步块中,总耗时都是0.1*200=20ms。

那么,

第一种情况:首次访问时,第一条线程锁住getInstance(),并执行1s的实际构造逻辑并返回实例(耗时1.0001秒),其他199条线程依次排队。在单例构造好后再依次获取实例,最慢一条线程耗时1.02秒。 二次访问时,所有线程都依次排队获取实例,最慢一条线程耗时0.02秒

第二种情况:在类装载时,构造单例实例耗时1s。首次访问时,200条线程并发获取实例,由于在cpu上还是要排队,最慢一条线程耗时0.02秒。(也就是说,从系统启动到首次访问完成,耗时1.02秒)。二次访问时,所有线程都并发获取实例,最慢一条线程耗时0.02秒。

第三种情况:首次访问时,与第一种情况类似(因为所有线程都通过了instance == null,在同步块上排队),最慢线程耗时1.02秒。二次访问时,所有线程都并发获取实例,但由于增加了条件判断(假设判断跳转时间为1ms),最慢一条线程耗时约为0.04秒。

可以看出,如果假定并发对基本操作时间无影响时,综合性能第一种和第二种一致,第三种最低,但差别均在微秒级别,事实上可以忽略。

需要注意的是,对于一个单例来说,上面例子中的线程总数500是个多余量,对结果完全没有影响。换句话说,是否延迟加载对于单个单例的性能没有影响。

如果假定jdk的实现使得基本操作在并发环境下性能较高,假设返回结果与条件判断的平均性能在并发下能提高1倍(双核?),变成0.05ms。那么,
第一种情况(完全没有利用并发):首次访问最慢线程:1.02秒,二次访问最慢线程0.02秒。
第二种情况:类载入时:1秒,首次访问最慢线程0.01秒(系统启动到完成首次访问总耗时1.01秒),二次访问最慢线程0.01秒
第三种情况:首次访问最慢线程1.01秒,二次访问最慢线程0.02秒。

同样可以看出,三种情况即使有差别,但差别均在微秒级别,事实上可以忽略。而且如果要较真的话,第三种情况还可能会慢,关键在于并发环境下返回结果的性能提升能否抵消多出来的两次判断跳转。

上面讨论的是一个单例被多条线程同时使用的情况。延时读取是考虑的是系统中存在大量不同单例的某些特殊情况。例如即时加载的劣势,体现在如果系统中有50个不同的单例类,而在某条执行路线上只载入了其中2个,即时加载会导致其他48个单例实例被无谓地创建。问题是在系统中定义了大量的单例类(要注意单例是难以继承的),而主线逻辑中又只使用了其中少量的现实场景究竟有多少。

综上,个人感觉在一般场景中,基本延迟加载方案与基本即时加载方案的效果是差不多的,完全可以根据个人喜好选择。除非已知系统中会出现“单例类爆炸”,而主流程又只使用其中少量的情况,那么就需要在团队中强制推行延迟加载方案。DoubleCheck同步加载方案需要证明“返回结果这种基本操作在并发环境下能带来实质的性能提升,足以抵消额外的条件判断的性能损耗”的前提下,才值得推广。

希望进行过实测的朋友来谈谈经验。

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

继续想了一下,似乎在这里使用DoubleCheck方式的主要出发点是”避免同步锁自身的初始化开销”,而不是”避免被锁住内容的执行开销“。那么问题就变为”同步锁自身的初始化开销,是否足以抵消额外的条件判断的性能损耗“。看来还是要实测一下。
分享到:
评论
14 楼 taolei0628 2010-12-25  
关于是不是性能瓶颈,有没有必要优化的问题,应该是针对比较复杂或有副作用的方式方法,简单有效的好方法,还是能用尽量用。
我们在这里讨论的都是业务无关的东西,写程序、做设计不重视细节的话,永远做不出好产品。
13 楼 kidneyball 2010-12-25  
楼上关于翻译的问题说得好,但"双重检查同步锁"又太长,已经统一更正为DoubleCheckLock。谢谢指出。

DoubleCheckLock的理论性能比方法同步锁快是肯定的,在很多非静态方法中进行延迟实例化,我也喜欢用DoubleCheckLock。这里讨论的是在单例这种特殊场景下,性能差别是否关键的。毕竟在写程序时,我们不希望关注太多业务无关的概念,除非真的有必要。
12 楼 taolei0628 2010-12-25  
DoubleCheck翻译成双锁?还真是会发明名词。
Java内存模型允许写入无序?N年前的东西还拿出来,写程序的都成考古的了。
如果还有遗留的JVM上存在所谓写入无序的bug,完全可以用简单的方式解决。
无论如何,对单例模式来说,DoubleCheck是个比较理想的方式。在高并发的情况下,同步锁竞争的开销是非常巨大的,况且单例是一次写,多次读。
Object instance = null;
Object getInstance()
{
    if(instance==null)
    {
        synchronized(thisOrClass)
        {
           if(instance == null)
           {
               instance = createInstance();
           }
        }
    }
}
/*
不直接使用new,即使局部存在写入无序的bug,也不会影响到返回结果
支持inline的JVM都比较新,没有这样的bug。
*/
Object createInstance()
{
    return new ...;
}
11 楼 kidneyball 2010-12-25  
一个可能性比较大的原因是,在代码内部直接使用静态类方法来获取对象实例会导致该段代码难以进行单元测试。(因为无法注入通过静态类方法来获取到的对象的mock)

因此在使用了单元测试的项目中,我们倾向于仅仅在某些粗粒度的阶段的起始位置(例如servlet的初始化阶段,或者刚刚开始处理请求时)用单例上的静态类方法来获取实例,之后会将这个实例作为方法参数传递下去,或者放入这个阶段的线程独立的上下文中,在后续的代码中直接通过上下文提取。

这样一来,本来在系统中直接调用静态方法来获取单例的机会就不多,而同步体内部的内容执行速度又快,仅仅是返回结果而已,刚好同时命中争夺资源的情况就少之又少了。强并发数在大到会在单例的getInstance方法上发生严重同步阻塞之前,早就在其他地方出现了严重的性能瓶颈了。
10 楼 kidneyball 2010-12-25  
其实我关心的是:有没有朋友在实际的项目开发中,使用了最简单、理论上性能最低的方法级同步锁来延迟获取单例,也就是第一种方案,后来经过了实际profiling测试,方法级同步锁确实造成了实际的性能瓶颈,改为DoubleCheck方式后,性能获得了实质提升的经验?

因为我平时接触JEE的应用比较多,而在很多JEE的应用或框架中,也比较少见使用DoubleCheck来做单例的,当然可能是JDK1.4的DoubleCheck的bug导致了这样的现状,但事实上也没有实际看到方法级同步锁单例造成性能瓶颈的情况,才引发了我上面的思考。或者在纯J2SE编程中有这样的场景?想听听各位朋友的看法。
9 楼 yangyi 2010-12-24  
mercyblitz 写道
melin 写道
public class ConcurrentSingleton {   
    private static final ConcurrentMap<String, ConcurrentSingleton> map = new ConcurrentHashMap<String, ConcurrentSingleton>();   
    private static ConcurrentSingleton instance;   
  
    public static ConcurrentSingleton getInstance() {   
        if (instance == null) {   
            instance = map.putIfAbsent("INSTANCE", new ConcurrentSingleton());   
        }   
        return instance;   
    }   
}   


public class AtomicBooleanSingleton {   
    private static AtomicBoolean initialized = new AtomicBoolean(false);   
    private static AtomicBooleanSingleton instance;   
       
    public static AtomicBooleanSingleton getInstantce() {      
        checkInitialized();   
        return instance;      
    }   
       
    private static void checkInitialized() {   
        if(instance == null && initialized.compareAndSet(false, true)) {   
            instance = new AtomicBooleanSingleton();   
        }   
    }   
}   



Keep simple,

这样地性能更不好,还不如直接AtomicX类来做,不过还是用地volatile做的!并且还有问题,    private static AtomicBooleanSingleton instance;   这里建议加一个volatile修饰一下!多个线程在观察这个变量地时候,没有volatile结果可能null和非null。


第一个,其实和直接new ConcurrentSingleton()是一样的,这个对象在放进map之前就被构造好了,所以可能会被重复构造多次,这和进不进map已经没有关系了,就算不进,大不了被GC;而且这样实现还可能导致不完全构造的问题。
第二个,有可能会产生空指针异常,同时也存在潜在的不完全构造问题
8 楼 mercyblitz 2010-12-24  
melin 写道
public class ConcurrentSingleton {   
    private static final ConcurrentMap<String, ConcurrentSingleton> map = new ConcurrentHashMap<String, ConcurrentSingleton>();   
    private static ConcurrentSingleton instance;   
  
    public static ConcurrentSingleton getInstance() {   
        if (instance == null) {   
            instance = map.putIfAbsent("INSTANCE", new ConcurrentSingleton());   
        }   
        return instance;   
    }   
}   


public class AtomicBooleanSingleton {   
    private static AtomicBoolean initialized = new AtomicBoolean(false);   
    private static AtomicBooleanSingleton instance;   
       
    public static AtomicBooleanSingleton getInstantce() {      
        checkInitialized();   
        return instance;      
    }   
       
    private static void checkInitialized() {   
        if(instance == null && initialized.compareAndSet(false, true)) {   
            instance = new AtomicBooleanSingleton();   
        }   
    }   
}   



Keep simple,

这样地性能更不好,还不如直接AtomicX类来做,不过还是用地volatile做的!并且还有问题,    private static AtomicBooleanSingleton instance;   这里建议加一个volatile修饰一下!多个线程在观察这个变量地时候,没有volatile结果可能null和非null。
7 楼 melin 2010-12-24  
public class ConcurrentSingleton {   
    private static final ConcurrentMap<String, ConcurrentSingleton> map = new ConcurrentHashMap<String, ConcurrentSingleton>();   
    private static ConcurrentSingleton instance;   
  
    public static ConcurrentSingleton getInstance() {   
        if (instance == null) {   
            instance = map.putIfAbsent("INSTANCE", new ConcurrentSingleton());   
        }   
        return instance;   
    }   
}   


public class AtomicBooleanSingleton {   
    private static AtomicBoolean initialized = new AtomicBoolean(false);   
    private static AtomicBooleanSingleton instance;   
       
    public static AtomicBooleanSingleton getInstantce() {      
        checkInitialized();   
        return instance;      
    }   
       
    private static void checkInitialized() {   
        if(instance == null && initialized.compareAndSet(false, true)) {   
            instance = new AtomicBooleanSingleton();   
        }   
    }   
}   
6 楼 test2002 2010-12-24  
推荐用第二种饿汉模式,加载的时候即初始化

忘了双重检查同步锁模式!



    在很多介绍java设计模式的书中都介绍到了单例模式,并且介绍过一种双重检查手段,用于提高效率。但是由于java内存模型原因,双重检测在并不能完全成功,在IBM发布的一篇文章具体介绍过失败的原因:http://www.ibm.com/developerworks/cn/java/j-dcl.html

最佳单例模式
       鉴于双重检查失败的原因,可以对单例模式中线程安全和延迟初始化进行改良,用一个内部类来解决问题,以下是源码:

public class Singleton
{
    private Singleton()
    {
    }

    /**
     * 双重检查会出现问题,故使用此种方法来处理线程安全 只有当使用的时候,类才会被初始化。
     *
     */
    private static class SingletonInn
    {
        static Singleton instance = new Singleton();
    }

    public static Singleton getInstance()
    {
        return SingletonInn.instance;
    }

}


5 楼 yangyi 2010-12-24  
http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
4 楼 kidneyball 2010-12-24  
mercyblitz 写道
如果构造Object的时间你都计较的话,C++也会表示压力巨大。

Java构造对象时,和C++构造没有区别。

时间上,非常快!


我指的是构造器里的逻辑,也就是同步锁事实上要保护的那一次执行。

第三种实现我是随便找了一个帖子复制过来的,因为我平时只用第一种。。。
3 楼 kidneyball 2010-12-24  
kinjo 写道
lz你的第一第二是看应用场景的,区别不仅仅是性能开销上的,二是实例的产生时机上,因为真正需要同步的就是产生实例那个时候的一次而已,但是这样同步是每次取都需要的,也就是同步的开支上不是很科学,第三种你的代码是有问题的,为什么叫双锁呢?很简单,同步了(锁)2次而已,其实我最常用的是用静态内部类来实现单例


延迟加载和即时加载在实例的产生时机上肯定是不同的。确实有很多明确需要延迟载入的场景,例如单例的构造函数逻辑依赖了外部的上下文,而你又很确定首次使用该单例时外部上下文是符合期待的。例如在web开发里就经常需要使用延迟载入,并传入request对象来获取request scope内的单例。考虑业务需求的话,只需要知道一种即时,一种延时的实现方案就行了。

极端点的话,感觉第一种延时方案基本上可以应付所有的场景了,事实上很少有必须使用即时载入的场景,因为在编写单例的时候,难以确定类是什么时候被载入的,在即时载入的单例构造器中加入依赖外部上下文的逻辑是不太合理的。暂时没想到出于应用场景的考虑必须使用即时载入的情况,有知道的朋友请提醒一下。:)

我思考的出发点在于,目前大部分关于单例的探讨,都是从性能开销上展开的。但如果撇开业务需求不谈,仅仅从性能上看,真的有那么大差别吗?为了一些根本没可能成为性能瓶颈的”性能问题“,记住好几种单例实现的变种和适用场景是否值得。

第三种实现我是随便找了一个帖子复制过来的,因为我平时只用第一种。。。
2 楼 mercyblitz 2010-12-24  
如果构造Object的时间你都计较的话,C++也会表示压力巨大。

Java构造对象时,和C++构造没有区别。

时间上,非常快!

特别指出一点,

真正的Double Check Lock是利用volatile 和synchronized配合的,你的代码有问题的。


ClassLoader来做看是比较好,实际上没有比synchronized快多少,因为临界区很小的!
1 楼 kinjo 2010-12-24  
lz你的第一第二是看应用场景的,区别不仅仅是性能开销上的,二是实例的产生时机上,因为真正需要同步的就是产生实例那个时候的一次而已,但是这样同步是每次取都需要的,也就是同步的开支上不是很科学,第三种你的代码是有问题的,为什么叫双锁呢?很简单,同步了(锁)2次而已,其实我最常用的是用静态内部类来实现单例

相关推荐

    43丨单例模式(下):如何设计实现一个集群环境下的分布式单例模式?1

    单例模式是一种设计模式,旨在确保一个类只有一个实例,并提供全局访问点。在单例模式中,类的构造函数是私有的,防止外部直接创建对象,而是通过静态方法获取该类的唯一实例。单例模式的唯一性通常是在进程范围内,...

    设计模式单例模式和工厂模式综合应用

    "设计模式单例模式和工厂模式综合应用"的主题聚焦于两种常用的设计模式:单例模式和工厂模式,并探讨它们如何协同工作来实现高效、灵活的代码结构。这个主题尤其适用于Java编程语言,因为Java的面向对象特性使得设计...

    设计模式之单例模式(结合工厂模式)

    单例模式是软件设计模式中的一种经典模式,它保证了类只有一个实例存在,并提供一个全局访问点。在Java等面向对象编程语言中,单例模式常用于管理共享资源,如数据库连接池、线程池或者配置文件等。结合工厂模式,...

    关于单例模式的知识要点

    单例模式是软件设计模式中的一种,其主要目的是控制类的实例化过程,确保一个类在整个应用程序中只有一个实例存在。这种模式在很多场景下都非常有用,例如管理共享资源、配置对象或者全局日志服务等。下面我们将深入...

    使用C++11实现线程安全的单例模式

    在C++编程中,单例模式是一种常用的软件设计模式,它保证一个类只有一个实例,并提供一个全局访问点。线程安全的单例模式在多线程环境下尤其重要,因为不正确的实现可能导致多个线程创建多个实例,这违反了单例模式...

    设计模式——单例模式

    以上就是关于单例模式的一些核心概念和常见实现方式。在实际开发中,根据需求和场景选择合适的实现方式至关重要,以平衡性能和资源使用。同时,需要注意的是,过度依赖单例可能导致系统设计过于紧密,不利于测试和...

    java单例模式实例

    单例模式是软件设计模式中的一种经典模式,用于确保一个类只有一个实例,并提供一个全局访问点。在Java中,有多种实现单例模式的方法,每种都有其特点和适用场景。接下来,我们将深入探讨这些实现方式。 首先,我们...

    C#单例模式详解 C#单例模式详解C#单例模式详解

    单例模式是软件设计模式中的一种,它保证一个类只有一个实例,并提供一个全局访问点。在C#中,单例模式常用于管理共享资源或控制类的实例化过程,以提高性能、节约系统资源,特别是在整个应用程序生命周期内只需要一...

    Java SE程序 单例模式

    Java SE程序 单例模式Java SE程序 单例模式Java SE程序 单例模式Java SE程序 单例模式Java SE程序 单例模式Java SE程序 单例模式Java SE程序 单例模式Java SE程序 单例模式Java SE程序 单例模式Java SE程序 单例模式...

    7种单例模式

    单例模式是软件设计模式中的一种经典模式,其主要目的是确保一个类只有一个实例,并提供一个全局访问点。这种模式在很多场景下非常有用,比如控制共享资源、管理配置对象等。下面将详细介绍七种常见的单例模式实现...

    单例模式详解~~单例模式详解~~

    单例模式是一种设计模式,它的主要目标是确保一个类只有一个实例,并提供一个全局访问点。在软件工程中,单例模式常用于控制资源的共享,比如数据库连接池、线程池或者日志系统等,这些资源通常需要全局唯一且高效地...

    23钟设计模式之单例模式

    总结来说,单例模式在Java中的实现涉及到多线程同步、内存模型以及性能优化等多个方面。理解并熟练掌握各种单例模式的实现方式,有助于我们编写出更加健壮、高效的代码。同时,设计模式的应用不仅仅局限于单例,还有...

    设计模式单例模式

    接下来,我们将详细讨论单例模式的实现方式,包括懒汉式和饿汉式,以及如何通过同步枷锁来保证线程安全。 1. **懒汉式**(Lazy Initialization):懒汉式单例模式的特点是在第一次需要时才创建对象,即延迟初始化。...

    使用单例模式创建学生管理系统(饿汉式、懒汉式)

    单例模式是软件设计模式中的一种,它的主要目的是确保一个类只有一个实例,并提供一个全局访问点。在Java或类似的面向对象编程语言中,单例模式常用于管理共享资源,如数据库连接池、线程池或者配置文件等。在这个...

    c++单例模式线程日志类

    在C++编程中,单例模式是一种常用的软件设计模式,它保证一个类只有一个实例,并提供一个全局访问点。在这个特定的场景中,我们讨论的是一个实现了单例模式的日志类,该类专为多线程环境设计,具备日志等级控制、...

    几种单例模式demo

    单例模式是软件设计模式中的一种,它的主要目的是确保一个类只有一个实例,并提供一个全局访问点。这种模式在很多场景下都非常有用,比如控制资源的唯一性、管理共享配置或者创建昂贵的对象时避免频繁创建销毁。 ...

    设计模式之单例模式源码demo

    4. **静态内部类单例模式**:利用Java类加载机制保证初始化实例时只有一个线程,避免了同步问题。同时,由于静态内部类不会随着外部类的加载而加载,所以也实现了延迟初始化。 ```java public class Singleton { ...

    Java 单例模式 工具类

    Java中的单例模式是一种常用的软件设计模式,它保证一个类只有一个实例,并提供全局访问点。在Java编程中,单例模式常用于控制资源的访问,比如数据库连接池、线程池或者日志对象等。本篇文章将深入探讨如何在Java中...

    Java 单例模式.pptx

    - **线程安全问题**:懒汉式单例模式在多线程环境下可能会导致创建多个实例,因此需要采用同步机制保证线程安全,例如使用`synchronized`关键字。 - **静态内部类方式** - **实现**: ```java class Single3 {...

    单例模式应用场景

    考虑到并发访问时数据同步的问题,通常会采用单例模式实现计数器逻辑。这种方式能够确保即使在高并发环境下,也能准确地记录每一次访问。 **4. 应用程序的日志处理** 在开发和运维阶段,日志记录对于追踪错误和...

Global site tag (gtag.js) - Google Analytics