线程安全的单例模式

Posted yangyongjie

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了线程安全的单例模式相关的知识,希望对你有一定的参考价值。

  双重检查锁与延迟初始化(懒汉式)

    在Java多线程程序中,有时候需要采用延迟初始化来降低初始化类和创建对象的开销,在使用这些对象时才进行初始化。延迟初始化需要注意线程安全

  问题,否则就容易出现问题。

    单例模式在获取实例的方法中,若只判断实例是否为null,是则创建对象,否则获取对象。这种方法在多线程执行的时候必然会有线程安全问题。若获取

  实例方法加synchronized关键字,则能实现线程同步解决线程安全问题,但是多线程下频繁调用会造成巨大的性能开销。

    1、双重检查锁及其错误根源

      双重检查锁是常见的延迟初始化技术,初衷是用它来降低同步的开销,但是它是错误的用法。

    双重检查锁代码:

public class DoubleCheckLock {
    private static Instance instance;
    public static Instance getInstance(){
        // 第一次检查
        if(instance==null){
            // 第一次检查为null再进行加锁,降低同步带来的性能开销
            synchronized (DoubleCheckLock.class){
                // 第二次检查
                if(instance==null){
                    // 问题出在此处
                    instance=new Instance();
                }
            }
        }
        return instance;
    }
}

 

     错误根源:

      当第一次检查时,读取instance不为null时,instance引用的对象可能还没有完成初始化!原因在于多线程下的重排序。

      instance=new Instance();  分为三步

        1)分配对象的内存空间

        2)初始化对象

        3)设置instance引用指向刚分配的内存地址

      但是在一些编译器上(如JIT),2)和3)可能会发生重排序。

      Java规范保证了重排序不会改变单线程内的程序执行结果。但是在多线程下,若线程A在执行instance=new Instance();时发生了重排序,先执行了3),

    这时候线程B刚好获取到了instance不为null,接着去访问对象。但是这个时候线程A还未执行2),即还没被线程A初始化,那么这个时候线程B得到的就是

    一个还没有初始化的对象。

      解决方案:

        (1)不允许2)和3)重排序

        (2)允许2)和3)重排序,但不允许其他线程“看到”这个重排序

    

    2、基于volatile的解决方案

     通过将instance声明为volatile型来禁止2)和3)之间的重排序

public class VolatileDoubleCheckLock {
    // 将instance声明为volatile型
    private volatile static Instance instance;
    public static Instance getInstance(){
        // 第一次检查
        if(instance==null){
            // 第一次检查为null再进行加锁,降低同步带来的性能开销
            synchronized (VolatileDoubleCheckLock.class){
                // 第二次检查
                if(instance==null){
                    // 多线程下将禁止2)和3)之间的重排序
                    instance=new Instance();
                }
            }
        }
        return instance;
    }
}

 

    3、基于类初始化的解决方案

     JVM在类的初始化阶段(即被Class加载后,且被线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁(Class对象的初始化锁)。这个锁可以同步多个线程对一个类的初始化。

public class InstanceFactory {
    private static class InstanceHolder{
        public static Instance instance=new Instance();
    }
    
    public static Instance getInstance(){
        return InstanceHolder.instance;
    }
}

    在多线程下,第一个执行getInstance方法的线程会先初始化InstanceHolder类。多个线程的情况下也只有一个线程能获取到InstanceHolder类的Class对象的初始化锁。第一个获取到nstanceHolder类的Class对象的初始化锁的线程将初始化InstanceHolder类中的静态属性instance。根据happens-before关系,其他线程将知道InstanceHolder类已被初始化,将结束初始化过程直接访问InstanceHolder。

 

以上是关于线程安全的单例模式的主要内容,如果未能解决你的问题,请参考以下文章

C# 两行代码实现 延迟加载的单例模式(线程安全)

C++ 线程安全的单例模式总结

线程安全的单例模式

Java线程安全的单例模式的几种实现

多线程 实现单例模式 ( 饿汉懒汉 ) 实现线程安全的单例模式 (双重效验锁)

java 实现线程安全的单例模式