`
qindongliang1922
  • 浏览: 2187832 次
  • 性别: Icon_minigender_1
  • 来自: 北京
博客专栏
7265517b-f87e-3137-b62c-5c6e30e26109
证道Lucene4
浏览量:117629
097be4a0-491e-39c0-89ff-3456fadf8262
证道Hadoop
浏览量:126029
41c37529-f6d8-32e4-8563-3b42b2712a50
证道shell编程
浏览量:59983
43832365-bc15-3f5d-b3cd-c9161722a70c
ELK修真
浏览量:71371
社区版块
存档分类
最新评论

Java单例模式之双检锁深入思考

    博客分类:
  • JAVA
阅读更多
# Java单例模式之双检锁剖析

### 前言

单例模式在Java开发中是非常经典和实用的一种设计模式,在JDK的内部包的好多api都采用了单例模式,如我们熟悉的Runtime类,单例模式总的来说有两种创建方式,一种是延迟加载的模式,一种是非延迟加载的模式,今天我们来学习一下基于双检锁延迟加载的单例模式。

### 什么是单例模式

顾名思义,单例模式指的是在整个程序运行期间,我们只能初始化某个类一次,然后一直使用这个实例,尤其是在多线程的环境下,也要保证如此。

### 基于双检锁的单例模式

在介绍基于双检锁的单例模式下,我们先思考下在使用延迟加载的情况下,如何实现一个单例模式,可能有一些比较年轻的小伙伴,不假思索的就写下了下面的一段代码:

```
    private  static DoubleCheckSingleton instance;
    
    //私有的构造方法
    private DoubleCheckSingleton() {}
    
    public static DoubleCheckSingleton getErrorInstance(){
        if (instance==null){
            instance=new DoubleCheckSingleton();
        }
        return instance;
    }
```

上面的代码在单线程的环境下是没有问题的,但是在多线程的环境下是不能保证只创建一个实例的,
然后小伙伴想了下,这还不简单,加个同步关键字就可以了:
```
    private  static DoubleCheckSingleton instance;
    
    //私有的构造方法
    private DoubleCheckSingleton() {}
    
    public synchronized static DoubleCheckSingleton getErrorInstance(){
        if (instance==null){
            instance=new DoubleCheckSingleton();
        }
        return instance;
    }
```


嗯,这下看起来没问题,但唯一的不足就是,这段代码虽然可以保证只创建一个单例,但其性能不高,因为每次访问这个方法的时候都需要执行同步操作,那么有没有方法可以避免这一个缺点呢?这个时候我们就可以用双检锁的模式了:


```
   private  volatile static DoubleCheckSingleton instance;
    
    //私有的构造方法
    private DoubleCheckSingleton() {}
    
    public static DoubleCheckSingleton getInstance(){

        if(instance==null){ //第一层检查

            synchronized (DoubleCheckSingleton.class){

                if(instance==null){ //第二层检查

                    instance=new DoubleCheckSingleton();

                }

            }

        }
        return instance;

    }
```


想要彻底理解双检锁模式的原理,首先要明白在Java里面一个线程对共享变量的修改,对于另外一个线程是不可预知的,也就是说它可能看不到变化,也有可能会看到,虽然在大多数时候是看不到的,但这不能证明它总是会被看到,除非正确的使用同步,否则是没法掌控的。

上面的基础认知非常重要,我原来就理解错误了,因为我通过代码检测出来,一个线程的修改对于另外一个线程是不可见的,所以就一直认为总是不可见的。但其实这是不正确的认识,因为编写多线程代码可能是容易的,但测试多线程程序是非常复杂的,或者说在一些情况下,没有人知道应该怎么测和怎么复现多线程bug,这也是多线程程序很难调试的的原因。


关于双检锁里面为什么必须要加volatile关键字,主要用来避免重排序问题导致其他的线程看到了一个已经分配内存和地址但没有初始化的对象,也就是说这个对象还不是处于可用状态,就被其他线程引用了。

下面的代码在多线程环境下不是原子执行的。

```
    instance=new DoubleCheckSingleton();
```



正常的底层执行顺序会转变成三步:

```java
(1) 给DoubleCheckSingleton类的实例instance分配内存

(2) 调用实例instance的构造函数来初始化成员变量

(3) 将instance指向分配的内存地址

```

上面的三步,无论在A线程当前执行到那一步骤,对B线程来说可能看到A的状态只能是两种1,2看到的都是null,3看到的非null,这是没问题的。

但是如果线程A在重排序的情况下,上面的执行顺序会变成1,3,2。现在假设A线程按1,3,2三个步骤顺序执行,当执行到第二步的时候。B线程开始调用这个方法,那么在第一个null的检查的时候,就有可能看到这个实例不是null,然后直接返回这个实例开始使用,但其实是有问题的,因为对象还没有初始化,状态还处于不可用的状态,故而会导致异常发生。

要解决这个问题,可以通过volatile关键词来避免指令重排序,这里相比可见性问题主要是为了避免重排序问题。如果使用了volatile修饰成员变量,那么在变量赋值之后,会有一个内存屏障。也就说只有执行完1,2,3步操作后,读取操作才能看到,读操作不会被重排序到写操作之前。这样以来就解决了对象状态不完整的问题。

那么volatile到底如何保证可见性和禁止指令重排序的

 在《深入理解Java虚拟机》一书中有描述:

  “观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”

  lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

```
   1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置, 
    也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时
    ,在它前面的操作已经全部完成;

  2)它会强制将对缓存的修改操作立即写入主存;

  3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
```

从上面可以看到volatile不保证原子性,保证可见性和部分有序性,这一点需要谨记。

此外这里需要注意的是在JDK5之前,就算加了volatile关键字也依然有问题,原因是之前的JMM模型是有缺陷,volatile变量前后的代码仍然可以出现重排序问题,这个问题在JDK5之后才得到解决,所以现在才可以这么使用。


正是因为双检锁的单例模式涉及的底层知识比较多,所以在面试中也是经常被问的一个话题。


### 其他的单例实现

前面说到过,单例模式从创建方式来说有懒汉(延迟加载)和非懒汉就是饿汉的单例模式。关于懒汉模式的除了双检锁模式,还有通过静态内部类实现的如下:

```
public class HolderFactory {
  public static Singleton get() {
    return Holder.instance;
  }

  private static class Holder {
    public static final Singleton instance = new Singleton();
  }
}
```

静态内部类是由JVM内部的锁机制来保证不会创建多个实例,非常巧妙的避开了多线程问题。

关于饿汉的单例模式形象点说,就是我不管你到底用不用得到都提前给你准备好。相比懒汉需要考虑各种线程问题,饿汉就比较简单了,第一种,非常简单:

```
    private static  SimpleSingleton ourInstance = new SimpleSingleton();

    public static SimpleSingleton getInstance() {
        return ourInstance;
    }

    private SimpleSingleton() {
    }
```


第二种,基于枚举方式:

```
public enum EnumSingleton {

    SINGLETON;
    

}

```

基于枚举的方式非常简洁,而且非常安全由jvm内部保证,自带私有的构造方法并且序列化和反射都不会破坏单例的安全性,据说是JDK5之后最好的单例创建方式,这个具体还是分应用场景。


### 总结

本篇文章重点介绍了在Java里面双检锁模式如何实现懒汉的单例模式,并分析其背后的原理和JMM的相关的一些知识,此外还介绍了其他的一些常用的单例模式供大家参考,感兴趣的小伙伴可以自己动手尝试一下。最后文中所有的代码已经上传到我的github,需要的朋友可以去fork运行。

https://github.com/qindongliang/Java-Note

















0
0
分享到:
评论

相关推荐

    Java 单例模式.pptx

    ### Java 单例模式详解 #### 一、什么是单例模式? 单例模式是一种常用的软件设计模式,在这种模式中,一个类只能拥有一个实例,并且该类必须自行创建并提供这个实例。通常,单例模式用于确保某个类在整个应用程序...

    java设计模式之单例模式.zip

    Java设计模式是面向对象编程中的重要概念,它们是软件开发中经过验证的、解决常见问题的最佳实践。...观看这些视频,可以更深入地理解并掌握Java中的单例模式,从而在实际开发中灵活运用,提升代码质量。

    《Java与模式 阎宏 摘录》.doc 更新中……

    在Java中,可以使用双检锁/双重检查锁定(Double-Checked Locking)或者静态内部类的方式来实现单例,以保证线程安全。 工厂模式则是一种创建型模式,它提供了一种创建对象的最佳方式,避免了在客户端代码中直接new...

    《java设计模式》课后习题模拟试题解答——刘伟.zip

    3. **模式间的相互关系**:了解不同设计模式之间的关联和区别,比如装饰器和代理模式的区别,或者单例模式与静态内部类的实现差异。 4. **模式的优缺点**:评估每种模式的适用性和潜在问题,如过度设计或性能影响。 ...

    java设计模式学习

    本资料“java设计模式学习”包含了对设计模式的深入理解和实际应用,通过简单实用的例子,帮助开发者掌握如何在Java项目中运用设计模式。 首先,我们要介绍的是工厂模式。工厂模式是一种创建型设计模式,它提供了一...

    Java与模式pdf

    2. **创建型模式**:包括单例模式、工厂模式(简单工厂、工厂方法、抽象工厂)、建造者模式、原型模式等,它们关注于如何创建对象,减少类之间的耦合。 3. **结构型模式**:如适配器模式、装饰器模式、代理模式、...

    用Java模式思考Thinking in Patterns with Java

    在深入探讨《用Java模式思考》(Thinking in Patterns with Java)这一主题之前,我们首先需要了解设计模式的基本概念以及它们如何被应用于Java编程语言中。本书不仅为读者提供了丰富的理论知识,还通过实际示例帮助...

    Java与模式.pdf

    另外,工厂模式常用于创建数据库连接池,如C3P0或DBCP,而单例模式常用于配置管理类,确保在整个应用中只有一个实例存在。 书中还会讨论到面向对象设计的原则,如单一职责原则(SRP)、开闭原则(OCP)、里氏替换...

    [Java设计模式(第2版)(Design.Patterns.in.Java).John.Metsker

    《java设计模式(第2版)》通过一个完整的java项目对经典著作design patterns一书介绍的23种设计模式进行了深入分析与讲解,实践性强,却又不失对模式本质的探讨。本书创造性地将这些模式分为5大类别,以充分展现各个...

    深入浅出设计模式(中文版)

    创建型模式关注对象的创建,如单例模式、工厂模式和建造者模式等,它们提供了不同方式来创建对象,使得代码更加灵活,易于维护。结构型模式处理对象组合和继承关系,如适配器模式、装饰器模式和代理模式等,这些模式...

    设计模式汇总_圣思园Java版

    在设计模式的学习中,UML类图用于描绘各个设计模式的结构和交互,如抽象工厂模式、单例模式、工厂方法模式等,通过图形化的方式使复杂的设计模式变得易于理解。 2. **Java源码**:每个设计模式都配有Java实现的源...

    java设计模式

    1. "Java与模式":这部分可能是一个文档或教程,详细解释了Java语言中各种设计模式的实现和应用,包括单例模式、工厂模式、观察者模式、装饰器模式等23种经典的GOF设计模式。 2. "《设计模式可复用面向对象软件的...

    Java设计模式+代码重构等PDF合集

    例如,单例模式保证一个类只有一个实例,而工厂方法则提供了一种创建对象的抽象方式,降低了耦合度。 第二版的《Java设计模式》中文版提供了丰富的例子和深入的解析,涵盖了23个经典的设计模式,不仅讲解了每个模式...

    Java设计模式视频03和04部分

    创建型模式关注对象的创建,如单例模式(Singleton)、工厂方法模式(Factory Method)和抽象工厂模式(Abstract Factory)。结构型模式涉及如何组合类和对象,比如适配器模式(Adapter)、装饰器模式(Decorator)...

    java与设计模式

    1. **创建型模式**:这类模式主要关注对象的创建过程,包括单例模式(Singleton)、工厂模式(Factory Method)、抽象工厂模式(Abstract Factory)、建造者模式(Builder)和原型模式(Prototype)。例如,单例模式...

    java二十二种设计模式pdf完整版最新版本

    2. 单例模式 3. 工厂方法模式 4. 建造者模式 5. 适配器模式 6. 桥接模式 7. 组合模式 8. 装饰器模式 9. 外观模式 10. 享元模式 11. 代理模式 12. 命令模式 13. 责任链模式 14. 解释器模式 15. 迭代器模式 16. 备忘录...

    java设计模式之外观模式.zip

    2. **子系统(SubSystem)角色**:这些是实际执行特定任务的类或对象,它们可以是其他设计模式的实例,如工厂模式、单例模式等。 3. **客户端(Client)角色**:通过调用外观角色的方法来与子系统进行交互,无需了解...

    设计模式的禅

    在Java中,可以通过静态内部类、枚举或者双检锁(Double-Check Locking)等方式实现。这种模式常用于配置管理、日志服务等场景,避免过多实例化导致资源浪费。 装饰器模式是一种结构型模式,可以在运行时动态地给...

    Java 中的设计模式 - jdon

    1. 创建型模式:这类模式关注对象的创建过程,如单例模式(Singleton)、工厂模式(Factory)、抽象工厂模式(Abstract Factory)、建造者模式(Builder)和原型模式(Prototype)。这些模式可以帮助我们控制对象的...

Global site tag (gtag.js) - Google Analytics