zoukankan      html  css  js  c++  java
  • 双重检查锁定的单例模式和延迟初始化

      有时候需要推迟一些高开销的对象初始化操作,并且只有在使用这些对象时才进行初始化。此时,常用的可能就是延迟初始化,例如:懒汉式单例模式,但是要正确的实现线程安全的延迟初始化需要一些技巧,下面是非线程安全的示例代码:

    public class UnsafeLazyInit {
        private static Instance instance ;
        
        public static Instance getInstance(){
            if(instance == null )             //1.A线程执行
                instance = new Instance() ;   //2.B线程执行
            
            return instance ;
        }
    }

      在示例代码中,假如A线程执行步骤1的同时,B线程执行步骤2,线程A可能会看到instance引用的对象还没有初始化完成。

      我们可以对getInstance()方法做同步处理来实现线程安全的延迟初始化。示例代码如下:

    public class UnsafeLazyInit {
        private static Instance instance ;
        
        public synchronized static Instance getInstance(){
            if(instance == null )            
                instance = new Instance() ;  
            
            return instance ;
        }
    }

      对getInstance()方法加上了synchronized关键字进行同步处理,这将导致线程获取锁和释放锁的开销,并且线程之间竞争锁会造成阻塞。如果getInstance()方法不会被多个线程频繁调用,那么这个方案也能够提供令人满意的性能。如果需要多线程频繁的调用,将会导致线程执行性能下降。

      进一步改进,可以使用双重检查锁定来实现延迟初始化。示例代码如下:

    public class UnsafeLazyInit {
        private static Instance instance ;     //1
                                               //2
        public synchronized static Instance getInstance(){  //3
            if(instance == null ){                          //4.第一次检查
                synchronized(UnsafeLazyInit.class){         //5.加锁
                    if(instance ==null )                    //6.第二次检查
                        instance = new Instance();             //7.初始化对象: 问题的根源
                }
            }         
            return instance ;
        }
    }

      如上代码所示,如果第一次检查结果不为null,那么就不需要进行加锁和初始化操作 。因此,可以大幅度降低synchronized带来的性能开销,看起来似乎两全其美:当多个线程试图在同一时间创建一个对象时,第5步代码通过加锁保证了只有一个线程能够创建对象。

    在对象创建好之后,执行getInstance()方法将不需要再次获得锁,直接返回创建的对象。

      但是以上代码还有一个错误的优化!当线程A执行到第7步时,线程B执行到第4步,这时候线程B读取到的instance可能不为null,但是instance的引用却还没完成初始化。

      在第7步创建一个对象,可以拆分为以下三行伪代码执行:

    1. memory = allocate() ;//分配对象的内存空间
    2. ctorInstance(memory) ;//初始化对象
    3. instance = memory ;//引用指向内存空间

      上述的伪代码,可能会被重排序,在JMM中,这种重排序是被允许的,它只保证重排序不会改变对单线程的执行结果,上述代码2、3步骤重排序不会影响单线程的执行结果,重排序之后的执行顺序如下:

    1. memory = allocate() ;//分配对象的内存空间
    
    3. instance = memory ;//引用指向内存空间
                                        //注意: 还没有初始化
    2. ctorInstance(memory) ;//初始化对象

      如果是单线程访问,重排序并不会影响最后的执行结果,如下图所示:

      下图表示多线程并发执行的情况:

     

      如上图,重排序只能保证线程A能够正确的访问对象,线程B可能访问到一个还没初始化完成的对象。

      在知晓了问题的根源之后,要实现线程安全的延迟加载,可以考虑以下两点:

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

      (2)允许2和3重排序,但是这个重排序对其他线程不可见。

    基于volatile的解决方案:

      只需要把以上示例的代码做一点小修改(instance声明为volatile型),就可以实现线程安全的延迟初始化。示例代码如下:

    public class UnsafeLazyInit {
        private volatile static Instance instance ; 
                                               
        public synchronized static Instance getInstance(){  
            if(instance == null ){                         
                synchronized(UnsafeLazyInit.class){        
                    if(instance ==null )                    
                        instance = new Instance(); //instance为volatile,会插入内存屏障,禁止重排序
                }
            }         
            return instance ;
        }
    }

      注意:以上方法需要JDK5或者更高的版本,JDK5之后使用新的内存模型JSR-133内存模型,增强了volatile的内存语义,使volatile和锁拥有相同的内存语义;

      

  • 相关阅读:
    使用parted对大于2T的磁盘进行分区
    iso系统镜像刻录到光盘和U盘
    戴尔R710服务器安装系统——配置raid
    UltraISO 9.7.1.3519注册码
    H3C交换机配置vlan
    kvm创建新虚拟机
    Windows添加永久静态路由
    gitlab部署步骤+汉化
    php配置php_pdo_mysql模块
    为git服务器配置gitosis管理权限
  • 原文地址:https://www.cnblogs.com/dquery/p/7077154.html
Copyright © 2011-2022 走看看