`
iavere
  • 浏览: 11059 次
  • 性别: Icon_minigender_1
  • 来自: 武汉
社区版块
存档分类
最新评论

多线程下单例模式(二)

阅读更多
Abstract

         在开发中,如果某个实例的创建需要消耗很多系统资源,那么我们通常会使用惰性加载机制,也就是说只有当使用到这个实例的时候才会创建这个实例,这个好处在单例模式中得到了广泛应用。这个机制在single-threaded环境下的实现非常简单,然而在multi-threaded环境下却存在隐患。本文重点介绍惰性加载机制以及其在多线程环境下的使用方法。(作者numberzero,参考IBM文章《Double-checked locking and the Singleton pattern》,欢迎转载与讨论)

1       单例模式的惰性加载
通常当我们设计一个单例类的时候,会在类的内部构造这个类(通过构造函数,或者在定义处直接创建),并对外提供一个static getInstance方法提供获取该单例对象的途径。例如:

Java代码  public class Singleton       
{       
    private static Singleton instance = new Singleton();       
    private Singleton(){       
        …       
    }       
    public static Singleton getInstance(){       
             return instance;        
    }       
}      





         这样的代码缺点是:第一次加载类的时候会连带着创建Singleton实例,这样的结果与我们所期望的不同,因为创建实例的时候可能并不是我们需要这个实例的时候。同时如果这个Singleton实例的创建非常消耗系统资源,而应用始终都没有使用Singleton实例,那么创建Singleton消耗的系统资源就被白白浪费了。

         为了避免这种情况,我们通常使用惰性加载的机制,也就是在使用的时候才去创建。以上代码的惰性加载代码如下:

Java代码  public class Singleton{       
    private static Singleton instance = null;       
    private Singleton(){       
        …       
    }       
    public static Singleton getInstance(){       
        if (instance == null)       
            instance = new Singleton();        
                return instance;        
    }       
}      


         这样,当我们第一次调用Singleton.getInstance()的时候,这个单例才被创建,而以后再次调用的时候仅仅返回这个单例就可以了。

2       惰性加载在多线程中的问题
先将惰性加载的代码提取出来:                           

Java代码  public static Singleton getInstance(){       
    if (instance == null)       
    instance = new Singleton();        
    return instance;        
}     

                                                                               


         这是如果两个线程A和B同时执行了该方法,然后以如下方式执行:

1.         A进入if判断,此时foo为null,因此进入if内

2.         B进入if判断,此时A还没有创建foo,因此foo也为null,因此B也进入if内

3.         A创建了一个Foo并返回

4.         B也创建了一个Foo并返回

此时问题出现了,我们的单例被创建了两次,而这并不是我们所期望的。

3       各种解决方案及其存在的问题
3.1     使用Class锁机制
以上问题最直观的解决办法就是给getInstance方法加上一个synchronize前缀,这样每次只允许一个现成调用getInstance方法:

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



    这种解决办法的确可以防止错误的出现,但是它却很影响性能:每次调用getInstance方法的时候都必须获得Singleton的锁,而实际上,当单例实例被创建以后,其后的请求没有必要再使用互斥机制了

3.2     double-checked locking
曾经有人为了解决以上问题,提出了double-checked locking的解决方案

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




         让我们来看一下这个代码是如何工作的:首先当一个线程发出请求后,会先检查instance是否为null,如果不是则直接返回其内容,这样避免了进入synchronized块所需要花费的资源。其次,即使第2节提到的情况发生了,两个线程同时进入了第一个if判断,那么他们也必须按照顺序执行synchronized块中的代码,第一个进入代码块的线程会创建一个新的Singleton实例,而后续的线程则因为无法通过if判断,而不会创建多余的实例。

         上述描述似乎已经解决了我们面临的所有问题,但实际上,从JVM的角度讲,这些代码仍然可能发生错误。

         对于JVM而言,它执行的是一个个Java指令。在Java指令中创建对象和赋值操作是分开进行的,也就是说instance = new Singleton();语句是分两步执行的。但是JVM并不保证这两个操作的先后顺序,也就是说有可能JVM会为新的Singleton实例分配空间,然后直接赋值给instance成员,然后再去初始化这个Singleton实例。这样就使出错成为了可能,我们仍然以A、B两个线程为例:

1.         A、B线程同时进入了第一个if判断

2.         A首先进入synchronized块,由于instance为null,所以它执行instance = new Singleton();

3.         由于JVM内部的优化机制,JVM先画出了一些分配给Singleton实例的空白内存,并赋值给instance成员(注意此时JVM没有开始初始化这个实例),然后A离开了synchronized块。

4.         B进入synchronized块,由于instance此时不是null,因此它马上离开了synchronized块并将结果返回给调用该方法的程序。

5.         此时B线程打算使用Singleton实例,却发现它没有被初始化,于是错误发生了。

4       通过内部类实现多线程环境中的单例模式
为了实现慢加载,并且不希望每次调用getInstance时都必须互斥执行,最好并且最方便的解决办法如下:

Java代码  public class Singleton{       
    private Singleton(){       
        …       
    }       
    private static class SingletonContainer{       
        private static Singleton instance = new Singleton();       
    }       
    public static Singleton getInstance(){       
        return SingletonContainer.instance;       
    }       
}      



       JVM内部的机制能够保证当一个类被加载的时候,这个类的加载过程是线程互斥的。这样当我们第一次调用getInstance的时候,JVM能够帮我们保证instance只被创建一次,并且会保证把赋值给instance的内存初始化完毕,这样我们就不用担心3.2中的问题。此外该方法也只会在第一次调用的时候使用互斥机制,这样就解决了3.1中的低效问题。最后instance是在第一次加载SingletonContainer类时被创建的,而SingletonContainer类则在调用getInstance方法的时候才会被加载,因此也实现了惰性加载。

分享到:
评论

相关推荐

    常见设计模式-单例模式

    单例模式是设计模式中的一种,其主要目的是确保一个类...5. 如何在保证单例模式的同时,增加可配置性或者实现不同实例的切换? 理解并熟练掌握单例模式的实现方式和应用场景,对于编写高效、可维护的代码至关重要。

    Head First设计模式(中文版)

    书中会展示如何避免多线程环境下单例的并发问题。 3. **建造者模式**:建造者模式将复杂对象的构建与其表示分离,使得同样的构建过程可以创建不同的表示。这有助于在不改变构造逻辑的情况下,修改产品的具体实现。 ...

    Java单例模式.docx

    在DCL中,`volatile`关键字是至关重要的,它能防止指令重排序,确保多线程环境下单例的正确初始化。 总结来说,单例模式在Java中应用广泛,尤其是在工具类、配置类等场景下。选择合适的实现方式取决于是否需要考虑...

    JAVA单例模式应用研究

    在这个版本中,`volatile` 关键字确保了多线程环境下单例对象的可见性和有序性。同时,`synchronized` 只在第一次创建实例时起作用,之后的调用将不再进行同步,大大提高了性能。 #### 六、结论 单例模式因其简单...

    单例窗体实现

    - **设计模式一致性**:虽然在某些场景下单例模式是合适的,但在其他情况下,可能需要考虑使用其他设计模式,如工厂模式或依赖注入,以提高代码的可测试性和灵活性。 通过以上介绍,我们可以理解单例窗体的实现原理...

    C#面试题

    - 在 `GetInstance` 方法内部增加锁机制来确保多线程环境下单例对象的唯一性。 - 使用 `lock` 关键字实现线程安全。 #### 五、数组排序 - 冒泡排序算法 - **算法描述**: - 遍历数组,比较相邻的两个元素。 - ...

    jmeter使用步骤及场景设计

    - **线程组3(下单)**:线程数为总线程数的20%,循环次数根据需要调整 - **线程组4(撤单)**:线程数为总线程数的10%,循环次数根据需要调整 - **线程组5(支付)**:线程数为总线程数的10%,循环次数根据需要调整...

    淘宝定点秒杀商品代码++++++++++++++++++++

    8. **多线程/异步编程**:如果同时处理多个秒杀商品,可以使用`concurrent.futures`库进行多线程或异步操作,提高抢购效率。 9. **日志记录**:为了追踪和调试程序,可以使用`logging`库记录程序运行过程中的重要...

    面试题,涵盖golong、mysql、redis、MongoDB、RabbitMQ、Kafka、Docker等等

    可以通过加锁来确保多线程环境下单例的正确性。 #### 7. 错误处理 Golang 中推荐使用错误作为第一个返回值的方式来处理错误。这种方式简洁明了,易于理解。对于复杂的错误处理场景,还可以考虑使用自定义错误类型...

    高并发业务场景下的秒杀解决方案.docx

    在并发量大的情况下,可能存在多个请求在库存查询和减少之间产生冲突,导致库存被不正确地扣减,从而引发超卖。例如,如果商品库存只剩1件,但两个请求几乎同时查询到库存为1,都尝试减少库存,最终可能会使库存变为...

    asp.net通过消息队列处理高并发请求(以抢小米手机为例)

    在并发情况下,多个用户可能会同时尝试购买同一款手机,导致数据不一致的问题。 为了解决并发问题,我们可以使用线程锁。在 ASP.NET Web 应用中,可以为可能产生并发冲突的代码段加上 `lock` 语句,确保同一时刻...

    开涛高可用高并发-亿级流量核心技术

    3.1 线程隔离 43 3.2 进程隔离 45 3.3 集群隔离 45 3.4 机房隔离 46 3.5 读写隔离 47 3.6 动静隔离 48 3.7 爬虫隔离 49 3.8 热点隔离 50 3.9 资源隔离 50 3.10 使用Hystrix实现隔离 51 3.10.1 Hystrix简介 51 3.10.2...

Global site tag (gtag.js) - Google Analytics