精华内容
下载资源
问答
  • 原文链接 ...在这补充一点,分析下volatile是怎么在单例模式中避免双检出现的问题的。 并发编程的3个条件 1 原子性:要实现原子性方式较多,可用synchronized、lock加锁,AtomicInteger等,但v...

    推荐:Java并发编程汇总

    双重检查锁单例模式为什么要用volatile关键字?

    原文链接

    前言

    从Java内存模型出发,结合并发编程中的原子性、可见性、有序性三个角度分析volatile所起的作用,并从汇编角度大致说了volatile的原理,说明了该关键字的应用场景;在这补充一点,分析下volatile是怎么在单例模式中避免双检锁出现的问题的。

    并发编程的3个条件

    1 原子性:要实现原子性方式较多,可用synchronized、lock加锁,AtomicInteger等,但volatile关键字是无法保证原子性的。
    2 可见性:要实现可见性,也可用synchronized、lock,volatile关键字可用来保证可见性。
    3 有序性:要避免指令重排序,synchronized、lock作用的代码块自然是有序执行的,volatile关键字有效的禁止了指令重排序,实现了程序执行的有序性。

    双重检查锁定模式

    双重检查锁定(Double check locked)模式经常会出现在一些框架源码中,目的是为了延迟初始化变量。这个模式还可以用来创建单例。下面来看一个 Spring 中双重检查锁定的例子。
    在这里插入图片描述
    这个例子中需要将配置文件加载到 handlerMappings中,由于读取资源比较耗时,所以将动作放到真正需要 handlerMappings的时候。我们可以看到 handlerMappings前面使用了volatile。有没有想过为什么一定需要 volatile?虽然之前了解了双重检查锁定模式的原理,但是却忽略变量使用了 volatile。
    下面我们就来看下这背后的原因。

    错误的延迟初始化例子

    想到延迟初始化一个变量,最简单的例子就是取出变量进行判断。
    在这里插入图片描述
    这个例子在单线程环境可以正常运行,但是在多线程环境就有可能会抛出空指针异常。为了防止这种情况,我们需要在该方法上使用 synchronized。这样该方法在多线程环境就是安全的,但是这么做就会导致每次方法调用都需要获取与释放锁,开销很大。
    深入分析可以得知只有在初始化的变量的需要真正加锁,一旦初始化之后,直接返回对象即可。
    所以我们可以将该方法改造以下的样子。
    在这里插入图片描述
    这个方法首先判断变量是否被初始化,没有被初始化,再去获取锁。获取锁之后,再次判断变量是否被初始化。第二次判断目的在于有可能其他线程获取过锁,已经初始化改变量。第二次检查还未通过,才会真正初始化变量。
    这个方法检查判定两次,并使用锁,所以形象称为双重检查锁定模式。
    这个方案缩小锁的范围,减少锁的开销,看起来很完美。然而这个方案有一些问题却很容易被忽略。

    new 实例背后的指令

    这个被忽略的问题在于 Cache cache=new Cache()这行代码并不是一个原子指令。使用 javap -c指令,可以快速查看字节码。
    在这里插入图片描述
    从字节码可以看到创建一个对象实例,可以分为三步:

    1. 分配对象内存。
    2. 调用构造器方法,执行初始化。
    3. 将对象引用赋值给变量。

    虚拟机实际运行时,以上指令可能发生重排序。以上代码 2,3 可能发生重排序,但是并不会重排序 1 的顺序。也就是说 1 这个指令都需要先执行,因为 2,3 指令需要依托 1 指令执行结果。
    Java 语言规定了线程执行程序时需要遵守 intra-thread semantics。intra-thread semantics 保证重排序不会改变单线程内的程序执行结果。这个重排序在没有改变单线程程序的执行结果的前提下,可以提高程序的执行性能。
    虽然重排序并不影响单线程内的执行结果,但是在多线程的环境就带来一些问题。
    在这里插入图片描述
    上面错误双重检查锁定的示例代码中,如果线程 1 获取到锁进入创建对象实例,这个时候发生了指令重排序。当线程1 执行到 t3 时刻,线程 2 刚好进入,由于此时对象已经不为 Null,所以线程 2 可以自由访问该对象。然后该对象还未初始化,所以线程 2 访问时将会发生异常。

    volatile 作用

    1. 正确的双重检查锁定模式需要需要使用 volatile。
    2. volatile主要包含两个功能。 保证可见性。使用volatile定义的变量,将会保证对所有线程的可见性。
    3. 禁止指令重排序优化。 由于volatile禁止对象创建时指令之间重排序,所以其他线程不会访问到一个未初始化的对象,从而保证安全性。

    注意,volatile禁止指令重排序在 JDK 5 之后才被修复。

    使用局部变量优化性能

    重新查看 Spring 中双重检查锁定代码。
    在这里插入图片描述
    可以看到方法内部使用局部变量,首先将实例变量值赋值给该局部变量,然后再进行判断。最后内容先写入局部变量,然后再将局部变量赋值给实例变量。
    使用局部变量相对于不使用局部变量,可以提高性能。主要是由于 volatile变量创建对象时需要禁止指令重排序,这就需要一些额外的操作。

    总结

    对象的创建可能发生指令的重排序,使用 volatile可以禁止指令的重排序,保证多线程环境内的系统安全。

    最后

    欢迎大家一起交流,喜欢文章记得点个赞哟,感谢支持!

    展开全文
  • /** * @since 2020/12/29 9:21 */ public class Singleton { //1.... //定义这个单例对象 private static volatile Singleton singleton; //对外提供方法 public static Singleton getSingl
    package com.aistrong.singleton;
    
    /**
     *  @since 2020/12/29 9:21
     */
    public class Singleton {
        //1.私有化构造器
        private Singleton() {
        }
    
        //定义这个单例对象
        private static volatile Singleton singleton;
    
    
        //对外提供方法
        public static Singleton getSingleton() {
            if (singleton == null) {
                synchronized (Singleton.class) {
                    if (singleton == null) {
                        singleton = new Singleton();
                    }
                }
            }
            return singleton;
        }
    
        public static void main(String[] args) {
            Singleton singleton = Singleton.getSingleton();
            Singleton singleton1 = Singleton.getSingleton();
            System.out.println(singleton == singleton1);
        }
    }
    
    
    展开全文
  • 这样可以极大提升并发度,进而提升性能,毕竟在单例中new的情况非常少,绝大多数都是可以并行的读操作,因此在加锁前多进行一次null检查就可以减少绝大多数的加锁操作,也就提高了执行效率。 但是必须注意的是...
    public class Singleton {
        private static volatile Singleton singleton = null;
    
        private Singleton(){}
    
        public static Singleton getSingleton(){
            if(singleton == null){
                synchronized (Singleton.class){
                    if(singleton == null){
                        singleton = new Singleton();
                    }
                }
            }
            return singleton;
        }    
    }
    

    进行两次null检查。这样可以极大提升并发度,进而提升性能,毕竟在单例中new的情况非常少,绝大多数都是可以并行的读操作,因此在加锁前多进行一次null检查就可以减少绝大多数的加锁操作,也就提高了执行效率。
    但是必须注意的是volatile关键字,该关键字有两层语义。第一层语义是可见性,可见性是指在一个线程中对该变量的修改会马上由工作内存(Work Memory)写回主内存(Main Memory),所以其它线程会马上读取到已修改的值,关于工作内存和主内存可简单理解为高速缓存(直接与CPU打交道)和主存(日常所说的内存条),注意工作内存是线程独享的,主存是线程共享的。volatile的第二层语义是禁止指令重排序优化,我们写的代码(特别是多线程代码),由于编译器优化,在实际执行的时候可能与我们编写的顺序不同。编译器只保证程序执行结果与源代码相同,却不保证实际指令的顺序与源代码相同,这在单线程并没什么问题,然而一旦引入多线程环境,这种乱序就可能导致严重问题。volatile关键字就可以从语义上解决这个问题,值得关注的是volatile的禁止指令重排序优化功能在Java 1.5后才得以实现,因此1.5前的版本仍然是不安全的,即使使用了volatile关键字。

    展开全文
  • 双重检测锁单例模式指令重排问题前言双重检测锁单例模式例子更改后的单例问题前因后果 前言 相信大多数同学在面试当中都遇到过写单例模式的题目,那么如何写一个完美的单例是面试者需要深究的问题,因为一个严谨的...

    前言

    相信大多数同学在面试当中都遇到过手写单例模式的题目,那么如何写一个完美的单例是面试者需要深究的问题,因为一个严谨的单例模式说不定就直接决定了面试结果,今天我们就要来讲讲看似线程安全的双重检测锁单例模式中可能会出现的指令重排问题。

    双重检测锁单例模式例子

    乍一看下面单例模式没啥问题,还加了同步锁保证线程安全,从表面上看确实看不出啥问题,当在同一时间多个线程同事执行该单例时就会出现JVM指令重排的问题,从而可能导致某一个线程获取的single对象未初始化对象。

    public class Single {
    
    	private static Single single;
    	
    	private Single() {
    	}
    	
    	public static Single getInstance() {
    		if(null == single) {
    			synchronized (Single.class) {
    				if(null == single) {
    					single = new Single();
    				}
    			}
    		}
    		return single;
    	}
    }
    

    问题前因后果

    其实single = new Single()这段代码并不具备原子性,从代码层面上来说确实没问题,但是如果了解JVM指令的就知道其实在执行这句代码的时候在JVM中是需要执行三个指令来完成的,如下:

    memory = allocate(); //1:分配对象的内存空间
    ctorInstance(memory); //2:初始化对象
    instance = memory; //3:设置instance指向刚分配的内存地址
    

    看到上面指令重排的解释之后,那么我们来回顾一下未加volatile修饰符的单例为何会出现问题。
    假设有A、B两个线程去调用该单例方法,当A线程执行到single = new Single()时,如果编译器和处理器对指令重新排序,指令重排后:

    memory = allocate(); //1:分配对象的内存空间
    instance = memory; //3:设置instance指向刚分配的内存地址,此时对象还没被初始化
    ctorInstance(memory); //2:初始化对象
    

    当A线程执行到第二步(3:设置instance指向刚分配的内存地址,此时对象还没被初始化)变量single指向内存地址之后就不为null了,此时B线程进入第一个if,由于single已经不为null了,那么就不会执行到同步代码块,而是直接返回未初始化对象的变量single,从而导致后续代码报错。

    解决方案

    问题也搞清楚了,接下来我们就来看下如何解决这个问题。
    解决问题的关键就在于volatile关键字,我们来看下volatile关键字的特性。
    可见性:

    • 写volatile修饰的变量时,JMM会把本地内存中值刷新到主内存 读
    • volatile修饰的变量时,JMM会设置本地内存无效

    有序性:
    要避免指令重排序,synchronized、lock作用的代码块自然是有序执行的,volatile关键字有效的禁止了指令重排序,实现了程序执行的有序性;
    看完volatile关键字的特性之后我们应该就明白了,是volatile关键字禁止了指令重排序从而解决了指令重排的问题。

    更改后的单例

    对比上面单例,下面单例在私有静态变量single前面加了修饰符volatile,能够防止JVM指令重排,从而解决了single对象可能出现成员变量未初始化的问题。

    public class Single {
    
    	private volatile static Single single;
    	
    	private Single() {
    	}
    	
    	public static Single getInstance() {
    		if(null == single) {
    			synchronized (Single.class) {
    				if(null == single) {
    					single = new Single();
    				}
    			}
    		}
    		return single;
    	}
    }
    
    展开全文
  • 今天小编就为大家分享一篇关于Java双重检查加锁单例模式的详解,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧
  • 多线程下,减少进入synchronized的等待队列的请求,提高效率。例如,已经创建了单例,线程T1进入,判断instance不为空,就不用进入同步代码块。 3、第二个if(instance null)的作用 防止生成多个实例。比如,...
  • 双重校验实现单例模式

    万次阅读 多人点赞 2019-08-21 15:11:04
    为什么是双重校验实现单例模式呢? 第一次校验:也就是第一个if(singleton==null),这个是为了代码提高代码执行效率,由于单例模式只要一次创建实例即可,所以当创建了一个实例之后,再次调用getInstance方法...
  • 单例模式 核心作用 保证一个类只有一个对象,并且提供一个访问该实例的全局访问点 单例模式的优点 由于单例模式只生成一个实例,减少了系统的开销,当一个对象需要比较的多的资源时,如读取配置、产生其它依赖...
  • Java双重校验实现单例模式

    万次阅读 多人点赞 2018-08-29 11:45:04
      这2种实现,在单线程模式下,也不会出现线程安全问题,但是,如果在多线程环境下,就可能出现线程安全问题,所以我们要对之前的代码进行改进—-双重校验。 代码实现   话不多说,咱们...
  • Java单例模式双重检查锁

    万次阅读 2018-08-01 11:38:29
    在努力创建更有效的代码时,Java 程序员们创建了双重检查锁定习语,将其和单例创建模式一起使用,从而限制同步代码量。然而,由于一些不太常见的 Java 内存模型细节的原因,并不能保证这个双重检查...
  • 双重校验单例模式代码如下: public class Singleton {  private static Singleton singleton;  private Singleton() {}  public static Singleton getSingleton() {  if (singleton == null) { // 1 ...
  • 双重检查加锁单例模式

    千次阅读 2015-02-14 20:15:48
    双重检查加锁单例模式为什么失效,多线程下怎样实现安全的单例模式。了解Java内存模型,同步的语义
  • Java单例模式双重检查锁的问题

    万次阅读 多人点赞 2016-06-17 19:16:58
    在努力创建更有效的代码时,Java 程序员们创建了双重检查锁定习语,将其和单例创建模式一起使用,从而限制同步代码量。然而,由于一些不太常见的 Java 内存模型细节的原因,并不能保证这个双重检查锁定习语有效。它...
  • 单例设计双重校验这种方式采用双锁机制,安全且在多线程情况下能保持高性能。但其中也有优缺点 双重校验代码 public class DoubleLock { private static DoubleLock doubleLock; private DoubleLock(){ ...
  • } 通过内部类实现多线程环境中的单例模式 为了实现慢加载,并且不希望每次调用getInstance时都必须互斥执行,最好并且最方便的解决办法如下: public class Singleton{ private Singleton(){ … } private static ...
  •  相信大多数同学在面试当中都遇到过手写单例模式的题目,那么如何写一个完美的单例是面试者需要深究的问题,因为一个严谨的单例模式说不定就直接决定了面试结果,今天我们就要来讲讲看似线程安全的双重...
  • 单例模式分为饿汉式和懒汉式。...比较稳妥的办法是在懒汉式的基础上加上,然后进行双重检查,这种SpringIOC容器式单例也是用这种双重检查来避免线程冲突导致的问题。 public class Singleton { private volatil...
  • 虽然这篇文章中也分析了如何利用同步机制保证懒汉式单例模式的线程安全问题,同步方法,同步代码块等,但都非最优的解决方法,今天我们就来讲讲什么是双重检验方式实现单例模式,包括它的特点和原理。...
  • Java中的双重检查锁(double checked locking) 在实现单例模式时,如果未考虑多线程的情况,就容易写出下面的错误代码: public class Singleton { private static Singleton uniqueSingleton; private ...
  • 在了解完volatile关键字之后,再仔细思考了单例模式双重检测,发现以前挺多东西还没懂的。 DCL(Double Check Lock) public class Singleton { private volatile static Singleton uniqueInstance; private ...
  • 内层判断:如果内层不加if判断,就会实例化多次,这是显而易见的,这就违背了单例模式的单例二字。 外层判断:试图想想一种情况,当线程1走完了内层判断,对象实例化了,线程3也调用了getInstace函数,如果没有加...
  • 单例模式双重检查锁

    2018-10-03 15:46:53
    单例模式基本上为面试必考内容,单例模式说简单也简单,但是在面试中还是有些细节需要注意,下面提供一个懒汉的、线程安全的、避免jvm指令重排的单例书写方式,需要注意的一些点在注释中进行了说明。 ​ public ...
  • 之前我们使用单例模式经常会用到两种方式,一种是懒汉是双重锁,一种是静态内部类,可以看下这个深入理解单例模式:静态内部类单例原理,但是其中用到的双重锁懒汉模式 其实并非是真正的线程安全的。 例如这样: ...
  • volatile+双重检查 实现单例模式

    千次阅读 热门讨论 2020-12-16 16:25:48
    提供一个方法进行双重检查机制返回单例对象 4.使用volatile修饰静态的变量 双重检查的优点:线程安全,延迟加载,效率较高! */ public class Singleton { private volatile static Singleton INSTAN
  • * 双重锁检查优化,采用的是volatile来检查 * @author xiaoyi * @date 2020/5/24 14:57 */ public class DoubleCheckedLocking { private volatile static DoubleCheckedLocking instance; public static ...
  • 在设计模式中,有一个很经典的模式-单例模式,它可能是实现上最简单的模式,在代码中也经常使用,在单线程下,毫无疑问延迟化加载是比较常用的,但是在多线程条件下,单例模式的延迟加载可能就会出现一些问题。...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 20,136
精华内容 8,054
关键字:

双重检查锁的单例模式