zoukankan      html  css  js  c++  java
  • 多线程场景下延迟初始化的策略

    1.什么是延迟初始化

    延迟初始化(lazy initialization,即懒加载)是延迟到需要域的值时才将它初始化的行为。如果永远不需要这个值,这个域就永远不会被初始化。这种方法既静态域,也适用于实例域。

    最好建议“除非绝对必要,否则就不要这么做”。

    2.延迟初始化线程安全的一个策略:同步

    延迟初始化的一个好处,是当域只在类的实例部分被访问,并且初始化这个域的开销很高,那就可能值得进行延迟初始化。

    但是在大多数情况下,正常的初始化要优先于延迟初始化。因为在多线程的场景下,采用某种形式的同步是很重要的,否则就容易造成严重的Bug。

    如下面的域bar,采取了延迟初始化的方法,那么在获取的时候,必须加上同步。

     1 class Foo {
     2     
     3     private Bar bar;
     4     
     5     synchronized Bar getBar() {
     6         if(null == bar) {
     7             bar = new Bar();
     8         }
     9         return bar;
    10     }
    11 }

    代码段-1

    否则的话,将上面的synchronized关键字去掉,考虑这样的场景:

    假设线程AB同时访问getBar()方法,下面表示线程执行代码的顺序,数字表示第几行。

    A->6, B->6, A->7, A->9, B->7, B->9;

    结果A,B两个线程获得的Bar对象,并不是同一个对象,有些情况下是允许的,但有些情况下很可能会引发问题,比如下面会讲到的单例模式。

    很明显,同步可以解决这个问题。但无论是synchronized关键字是加在方法上还是块里面,每次访问getBar()方法,都需要加锁,解锁,增加了开销。

    上面的案例对于静态域同样适用。

    综上所述,同步可以解决在多线程访问域时线程不安全的问题,但会增加额外的开销。所以这不是一个好的方法。

    3.静态域延迟初始化的策略:lazy initialization holder class模式

    如果出于性能的考虑而需要对静态域使用延迟初始化,就使用lazy initialization holder class模式(也称作initianlize-on-demand holder class idiom)。

    这种模式的写法如下:

     1 class Foo {
     2     
     3     //1. 一个私有静态类,将静态域放到静态类里
     4     private static class BarHolder {
     5         static final Bar bar = new Bar();        
     6     }
     7     
     8     //2. 访问域的时候,返回静态类的域
     9     public Bar getBar() {
    10         return BarHolder.bar;
    11     }
    12 }

    代码段-2

    当getBar()方法被调用时,它读取了BarHolder.bar的值,导致BarHolder类得到初始化。类初始化的时候,会初始化静态域bar. 

    对于多个线程来说,无论谁先读取BarHolder.bar的值,BarHolder都将只初始化一次,因此每次访问的结果都是同一个Bar对象。

    这种模式的魅力在于,getBar()方法没有被步,并且只执行一个域访问,因此延迟初始化实际上并没有增加任何访问成本。

    这种模式最常见的应用,就是在单例模式中。单例模式要求类本身有一个静态域(如instance),指向类的一个实例,该类的getInstance()返回该静态域,并且每次返回的都是同一个对象,这才是一个单例。

    假如使用延迟初始化,在getInstance的时候,没有添加同步,如下面所示。

     1 class Foo {
     2     
     3     private static Foo instance ;
     4     
     5     public static Foo getInstance() {
     6         if(null == instance) {
     7             instance = new Foo();
     8         }
     9         return instance;
    10     }
    11 }

    代码段-3

    很明显,多次访问可能返回不同的对象,而如果加上synchronized关键字,或者加上读写锁,则开销较大,每次获取一个对象的时候还要竞争锁。

    使用了lazy initialization holder class模式,就可以写成下面这样:

     1 class Foo {
     2     
     3     private static class FooHolder {
     4         static final Foo instance = new Foo();
     5     }
     6     
     7     public static Foo getInstance() {
     8         return FooHolder.instance;
     9     }
    10 }

    代码段-4

    当然,最好的实践,还是不要采用延迟初始化。除非是初始化比较耗时而影响了系统的启动,比如我们公司采用OSGi框架,每个bundle启动的时间都要做优化,那么单例的初始化可以等到系统已经启动了,操作时用到再初始化。

    4.实例域延迟初始化的策略:双重检查模式(double-check idiom)

    这种模式避免了域在被初始化之后访问这个域时的锁定开销(参考代码段-1)。这种模式背后的思想是:两次检查域的值(因此为double check),第一次检查时没有锁定,看看这个域是否被初始化了;第二次检查时有锁定。只有当第二次检查时表明这个域没有被初始化,才会调用初始化方法。因为如果域已经被初始化就不会有锁定(域被声明为volatile很重要)。和代码段-1对比一下,一个每次访问都会锁定,无论域是否已经初始化,一个是存在若干次需要锁定,一旦域已经初始化,将不会再有锁定。可见,开销变小了。

     1 class Foo {
     2     //volatile关键字
     3     private volatile Bar bar;
     4     
     5     public Bar getBar() {
     6         //局部变量只是减少读取次数,提升性能,不是严格需要
     7         Bar result = this.bar;
     8         //第一次检查,不锁定
     9         if(null == result) {
    10             //一旦初始化,第一次检查将无法通过,不会有锁定开销
    11             synchronized (this) {
    12                 result = this.bar;
    13                 //第二次检查,锁定
    14                 if(null == result) {
    15                     this.bar = result = new Bar();
    16                 }
    17             }
    18         }
    19         return result;
    20     }
    21 }

    代码段-5

    注意:在Java 1.5发行之前,双重检查模式的功能很不稳定,因为volatile修饰符的主义不够强,难以支持它。Java 1.5版本引入的内存模式解决了这个问题(内存模式参考 原子性和可见性)。如今,双重检查模式是延迟初始化一个实例域的方法。

    有些时候,你可能需要延迟初始化一个可以接受重复初始化的实例域。如果处于这种情况,就可以使用双重检查模式的一个变形,省去第二次检查。这种叫做单重检查模式(single-check iidiom)。对实例域来说,代码如下:

     1 class Foo {
     2     // volatile关键字
     3     private volatile Bar bar;
     4 
     5     public Bar getBar() {
     6         // 局部变量只是减少读取次数,提升性能,不是严格需要
     7         Bar result = this.bar;
     8         // 只有一次检查
     9         if (null == result) {
    10             this.bar = result = new Bar();
    11         }
    12         return result;
    13     }
    14 }

    代码段-6

    对静态域来说,实际上,就是代码段-3。

    单重检查模式这种情况是可能发生的。例如最近开发的一个特性,通过工厂模式返回一个HttpClient的包装对象(该类是用来向一个固定的系统发送消息的,因此IP,端口,鉴权信息全都是固定的,全局只有一个对象就够了,客户端调用时也不用填写对端系统信息)。

    包装类对象延迟初始化。包装类的作用只是包装了一个第三方HttpClient,发送消息,返回响应消息,中间封装了一些操作,减少客户端的调用代码而已。对于客户端来说,这个对象不存储共享数据,使用后也没有存储起来,因此是允许重复初始化域的。

  • 相关阅读:
    MySQL语句创建表、插入数据遇到的问题-20/4/18
    【Navicat】MySQL 8.0.17 数据库报2059错误
    MySQL 8.0.17 版安装 || Windows
    C#--Invoke和BeginInvoke用法和区别
    C#--params关键字
    C#--typeof() 和 GetType()区别
    C#--利用反射编写的SqlHelper类
    C#--反射基础
    C#--LINQ--2--LINQ高级查询
    C#--LINQ--1--初学LINQ基础和查询
  • 原文地址:https://www.cnblogs.com/kingsleylam/p/6081586.html
Copyright © 2011-2022 走看看