精华内容
下载资源
问答
  • 今天小编就为大家分享一篇关于Java双重检查加锁单例模式的详解,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧
  • 双重检测锁单例模式指令重排问题前言双重检测锁单例模式例子更改后的单例问题前因后果 前言 相信大多数同学在面试当中都遇到过写单例模式的题目,那么如何写一个完美的单例是面试者需要深究的问题,因为一个严谨的...

    前言

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

    双重检测锁单例模式例子

    乍一看下面单例模式没啥问题,还加了同步锁保证线程安全,从表面上看确实看不出啥问题,当在同一时间多个线程同事执行该单例时就会出现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;
    	}
    }
    
    展开全文
  • 多线程下,减少进入synchronized的等待队列的请求,提高效率。例如,已经创建了单例,线程T1进入,判断instance不为空,就不用进入同步代码块。 3、第二个if(instance null)的作用 防止生成多个实例。比如,...
    class Singleton{
    	private static volatile Singleton instance=null;
    	private Singleton() {}
    	public static Singleton getInstance() {
    		if (instance==null) {
    			synchronized(Singleton.class) {
    				if (instance==null) {
    					instance=new Singleton();
    				}
    			}
    		}
    		return instance;
    	}
    }
    

    1、volatile的作用
    在instance=new Singleton();中,这段代码是分3步执行的
    (1)为instance分配内存空间
    (2)初始化instance
    (3)将instance指向分配的内存地址
    由于jvm具有指令重排序的特性,执行的顺序可能变成1->3->2,所以在多线程环境下,可能一个线程会得到还没有初始化的实例。比如,线程T1执行了1、3,线程T2来发现instance不为空,所以返回instance,但是instance还没有初始化,所以需要volatile禁止指令重排序。
    2、第一个if(instancenull)的作用
    多线程下,减少进入synchronized锁的等待队列的请求,提高效率。例如,已经创建了单例,线程T1进入,判断instance不为空,就不用进入同步代码块。
    3、第二个if(instance
    null)的作用
    防止生成多个实例。比如,线程T1还没有将instance指向分配的内存空间,线程T2在synchronized的等待队列中等待,当T1完成单例创建,然后如果没有if判断,T2也会生成单例实例,所以需要第二个if判断。

    展开全文
  • * @description 双重检验锁单例模式 */ public class Main { public static void main(String[] args) { Singleton a = Singleton.getInstance(); Singleton b = Singleton.getInstance(); System.o

    代码:

    /**
     * @author 士多啤梨西多士
     * @date 2020/9/9 23:51
     * @description 双重检验锁单例模式
     */
    public class Main {
        public static void main(String[] args) {
            Singleton a = Singleton.getInstance();
            Singleton b = Singleton.getInstance();
            System.out.println(a);
            System.out.println(b);
            System.out.println(a == b);
        }
    }
    
    class Singleton {
        private static volatile Singleton instance;
    
        private Singleton() {}
    
        public static Singleton getInstance() {
            if(instance == null) {
                synchronized (Singleton.class) {
                    if (instance == null){
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }
    
    展开全文
  • 单例模式 核心作用 保证一个类只有一个对象,并且提供一个访问该实例的全局访问点 单例模式的优点 由于单例模式只生成一个实例,减少了系统的开销,当一个对象需要比较的多的资源时,如读取配置、产生其它依赖...

    单例模式

    核心作用

    • 保证一个类只有一个对象,并且提供一个访问该实例的全局访问点

    单例模式的优点

    • 由于单例模式只生成一个实例,减少了系统的开销,当一个对象需要比较的多的资源时,如读取配置、产生其它依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决
    • 单例模式可以在系统设置全局访问点,优化环共享资源访问,例如可以设计一个单例类,负责所以数据表的映射处理

    常见的五种单例模式实现方式

    • 主要
      • 饿汉式(线程安全,调用效率高。但是,不能延时加载)
      • 懒汉式(线程安全,调用效率不高。但是,可以延时加载)
    • 其它
      • 双重检测锁式单例(由于JVM底层内部模型原因,偶尔会出问题。不建议使用)
      • 静态内部类实现(线程安全,调用效率高。但是,可以延迟加载)
      • 枚举单例(线程安全,调用效率高,不能延迟加载)

    饿汉式单例:

    饿汉式单例,顾名思义就是无论是否需要使用该类的实例,在类加载时都创建该类的实例。(并不是只有下面一种实现,下文的枚举单例也是饿汉式单例。)

    Java 代码实现如下:

    /**
     * 饿汉式单例模式
     */
    public class Singleton {
    
        //类初始化时立即加载
        private static final Singleton instance = new Singleton1();
    
        private Singleton1() {}
    
        public static Singleton getInstance() {
            return instance;
        }
    	
        //...
    }
    

    在该类被加载时实例就会被创建,无论是否要使用该类,所以称为饿汉式

    懒汉式单例:

    懒汉式单例,就是类加载时不创建类的实例,在第一次使用时再创建。(同样也不只有下面的一种实现,下文的静态内部类实现的单例以及双重检测锁式单例都是懒汉式单例。)

    Java 代码实现如下:

    /**
     * 懒汉式单例模式
     */
    public class Singleton {
    
        //类初始化时,不初始这个对象,用到的时候再创建(延时加载)
        private static Singleton instance = null;
    
        private Singleton() {}
    
        /**
         * 对象延迟加载,只要在要用的时候才会创建
         * 但是为了防止对象多线程时重复创建对象,所以必须加synchronized,效率不高,因为多线程时需要等待
         */
        public static synchronized Singleton getInstance() {
            if (instance == null) {
                instance = new Singleton();
            }
            return instance;
        }
    
        //...
    }
    

    在类加载时,实例不会被初始化,默认为null;当第一次调用 getInstance() 方法时才会创建实例;为了保证实例不被重复创建,对 getInstance() 方法添加了 synchronized 锁。但这种实现并不高效,因为任何时候调用都必须承受同步带来的性能开销,然而又只有第一次调用需要同步,所以并不建议使用。

    静态内部类实现单例模式

    Java 代码实现如下:

    /**
     * 静态内部类实现单例模式
     */
    public class Singleton {
    
        private static class SingletonClassInstance {
            private static final Singleton instance = new Singleton4();
        }
    
        private Singleton() {}
    
        public static Singleton getInstance() {
            return SingletonClassInstance.instance;
        }
    
        //...
    }
    

    因为内部类不会因为外部类的加载而加载,只有在使用到内部类时才加载,所以静态内部类的单例实现是延时加载且线程安全的(实例在且只在内部类被加载时创建),又因为没有添加同步锁,所以调用效率也高。

    枚举单例

    Java 代码实现如下:

    /**
     * 枚举单例
     */
    public enum Singleton5 {
    
        //枚举元素本身就是单例的
        INSTANCE;
    
        public void operation() {
            //...
        }
    
    }
    

    枚举元素天生就是线程安全的单例,调用效率也高,只是无法延时加载!

    双重检测锁式单例

    为了解决上述的懒汉式单例因为同步带来的性能损耗,聪明的程序员想到了使用双重检测锁来解决每次调用都需要同步的问题。尽管双重检测锁背后的理论是完美的,但不幸的是由于 Java 的内存模型允许“无序写入” , 错误的双重检测锁式单例并不能保证它会在单处理器或多处理器计算机上顺利运行。

    错误双重检测锁式单例:

    /**
     * 错误双重检测锁式单例
     */
    public class Singleton {
        
        private static Singleton instance = null;
        
        private Singleton() {}
        
        public static Singleton getInstance() {
            if (instance == null) {
                synchronized (Singleton.class) {
                    if (instance == null) {
                        instance = new Singleton();//erro
                    }
                }
            }
            return instance;
        }
    }
    

    下面是上述代码的运行顺序:

    1. 检测实例是否已经初始化创建,如果是则立即返回
    2. 获得锁
    3. 再次检测实例是否已经初始化创建成功,如果还没有则创建实例

    执行双重检测是因为,如果多个线程通过了第一次检测,并且其中一个首先通过了第二次检测并实例化了对象,剩余的线程不会再重复实例化对象。这样,除了初始化的时候会加锁,后续的调用都是直接返回,解决了多余的性能消耗。

    隐患

    看似天衣无缝,但是这种实现是有隐患的,这个隐患来自于上述代码中注释了 erro 的一行,这行代码大致有以下三个步骤:

    1. 在堆中开辟对象所需空间,分配地址
    2. 根据类加载的初始化顺序进行初始化
    3. 将内存地址返回给栈中的引用变量

    由于 Java 内存模型允许“无序写入”,有些编译器因为性能原因,可能会把上述步骤中的 2 和 3 进行重排序,顺序就成了

    1. 在堆中开辟对象所需空间,分配地址
    2. 将内存地址返回给栈中的引用变量(此时变量已不在为null,但是变量却并没有初始化完成)
    3. 根据类加载的初始化顺序进行初始化

    现在考虑重排序后,两个线程出现了如下调用:

    TimeThread AThread B
    T1第一次检测, instance 为空
    T2获取锁
    T3再次检测, instance 为空
    T4在堆中分配内存空间
    T5instance 指向分配的内存空间
    T6第一次检测,instance不为空
    T7访问 instance(此时对象还为初始化完成)
    T8初始化 instance

    此时 T7 时刻 Thread B 对 instance 的访问,访问到的是一个还未完成初始化的对象。所以在使用 instance 时可能会出错。

    解决无序写入问题的尝试

    基于上文的问题,自然而然的可以得出以下修复代码

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

    此代码的理论是通过一个局部变量和内部同步代码块,使创建实例在内部同步块中进行并赋值给局部变量,退出内部同步块时实例已经初始化完成,然后再从局部变量赋值给 instance,从而使 instance 引用内存空间时,指向的是一个已经初始化完成的实例。

    这个代码理论上是可行的,但是理论却与实际背道而驰。实际上这个代码并不是按照上述理论的步骤执行的, Java 语言规范要求不能将 synchronized块中的代码移出来。但是,并没有说不能将 synchronized 块外面的代码移 synchronized 块中。 编译器在这里会看到一个优化的机会,此优化会删除上面注释 * 的两行代码,组合并产生以下代码

    //编译器优化后的代码
    public class Singleton {
    
        private static Singleton instance = null;
    
        private Singleton() {}
    
        public static Singleton getInstance() {
            if (instance == null) {
                Singleton temp;
                synchronized (Singleton.class) {
                    temp = instance;
                    if (temp == null) {
                        synchronized (Singleton.class) {
                            //temp = new Singleton();
                            instance = new Singleton();
                     }
                     //instance = temp;
                 }
             }
         }
         return instance;
        }
    }
    

    同样还是会遇到之前无序写入导致的问题。

    正确的双重检测锁式单例

    /**
     * 正确的双重检测锁实现单例模式
     */
    public class Singleton3 {
    
        private static volatile Singleton3 instance = null;
    
        private Singleton3() {}
    
        public static Singleton3 getInstance() {
            if (instance == null) {
                synchronized (Singleton3.class) {
                    if (instance == null) {
                        instance = new Singleton3();
                    }
                }
            }
            return instance;
        }
    }    
    

    为了解决上述问题,需要在instance前加入关键字volatile。使用了volatile关键字后,重排序被禁止,所有的写(write)操作都将发生在读(read)操作之前。 但是只在 JDK5 及之后有效。

    JDK5 以及后续版本扩展了volatile语义,不再允许volatile写操作与其前面的读写操作重排序,也不允许volatile读操作与其后面的读写操作重排序。

    总结

    上述五种常见的单例模式实现中,如果需要延时加载建议使用静态内部类实现,至于饿汉式单例两种实现都可以。

    原文请点击此处
    展开全文
  • 双重校验实现单例模式

    万次阅读 多人点赞 2019-08-21 15:11:04
    为什么是双重校验实现单例模式呢? 第一次校验:也就是第一个if(singleton==null),这个是为了代码提高代码执行效率,由于单例模式只要一次创建实例即可,所以当创建了一个实例之后,再次调用getInstance方法...
  • 前言 ...在这补充一点,分析下volatile是怎么在单例模式中避免双检出现的问题的。 并发编程的3个条件 1、原子性:要实现原子性方式较多,可用synchronized、lock加锁,AtomicInteger等,但volati...
  • Java双重校验实现单例模式

    万次阅读 多人点赞 2018-08-29 11:45:04
      这2种实现,在单线程模式下,也不会出现线程安全问题,但是,如果在多线程环境下,就可能出现线程安全问题,所以我们要对之前的代码进行改进—-双重校验。 代码实现   话不多说,咱们...
  • 由于普通写法的懒汉式单例模式在多线程情况下是不安全的,所以出现了安全的懒汉式单例模式的写法,即双重检验锁模式。 class Singleton{ // 确保产生的对象完整性 private volatile static Singleton ...
  • 双重校验实现对象单例(线程安全) public class Singleton { private volatile static Singleton singleton; private Singleton() {} public static Singleton getInstance() { if (singleton == null) { ...
  • 单例模式双重检验锁

    千次阅读 2018-03-23 11:00:24
    当被问到要实现一个单例模式时,很多人的第一反应是写出如下的代码,包括教科书上也是这样教我们的。 public class Singleton { private static Singleton instance; private Singleton (){} public static ...
  • 在了解完volatile关键字之后,再仔细思考了单例模式双重检测,发现以前挺多东西还没懂的。 DCL(Double Check Lock) public class Singleton { private volatile static Singleton uniqueInstance; private ...
  • 单例模式就是一个类只有一个实例。 单例模式的两个特点: 构造方法私有,外部不能直接new出对象。 提供一个 public 的静态方法,使得外部通过该方法获取单例类的实例。 单例模式根据实例的创建时机,大致又分为两...
  • 面试题:双重检验锁方式实现 单例模式 关键词 volatile 禁⽌ JVM 中 构造方法的 指令重排 编码实现 public class Singleton { private volatile static Singleton instance; private Singleton() { } ...
  • Java单例模式双重检查

    万次阅读 2018-08-01 11:38:29
    在努力创建更有效的代码时,Java 程序员们创建了双重检查锁定习语,将其和单例创建模式一起使用,从而限制同步代码量。然而,由于一些不太常见的 Java 内存模型细节的原因,并不能保证这个双重检查...
  • 双重检查的两层判断及synchronized的意义? 有谁能解释清楚的么?</p>
  • 单例模式双重检测

    千次阅读 2020-04-07 17:56:58
    1.一般的单例模式如下: class Singleton{ private static Singleton singleton; private Singleton(){} public static Singleton getInstance(){ if(singleton == null){ singl...
  • 为什么单例模式使用双重检验锁

    千次阅读 2020-07-10 14:22:39
    * 单例模式-双重校验 * @author szekinwin * */ public class SingleTon3 { private SingleTon3(){}; //私有化构造方法 private static volatile SingleTon3 singleTon=null; public static SingleTon3 ...
  • Java单例模式双重检查的问题

    万次阅读 多人点赞 2016-06-17 19:16:58
    在努力创建更有效的代码时,Java 程序员们创建了双重检查锁定习语,将其和单例创建模式一起使用,从而限制同步代码量。然而,由于一些不太常见的 Java 内存模型细节的原因,并不能保证这个双重检查锁定习语有效。它...
  • public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new...
  • 虽然这篇文章中也分析了如何利用同步机制保证懒汉式单例模式的线程安全问题,同步方法,同步代码块等,但都非最优的解决方法,今天我们就来讲讲什么是双重检验锁方式实现单例模式,包括它的特点和原理。...
  • 双重检查二次判空原因 第一次判断是为了验证是否创建对象 第二次判断是为了避免重复创建单例,因为可能会存在多个线程通过了第一次判断在等待,来创建新的实例对象。

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 10,963
精华内容 4,385
关键字:

双重检验锁单例模式