ITEM 83: 谨慎使用延迟初始化

ITEM 83: USE LAZY INITIALIZATION JUDICIOUSLY
  延迟初始化是将字段的初始化延迟到需要它的值时。如果不需要该值,则不会初始化该字段。这种技术既适用于静态字段,也适用于实例字段。虽然延迟初始化主要是一种优化,但它也可以用来打破类和实例初始化中的有害循环 [Bloch05, Puzzle 51]。
  与大多数优化一样,对于延迟初始化,最好的建议是 “除非需要,否则不要做” (item 67)。延迟初始化是一把双刃剑。它减少了初始化类或创建实例的成本,但增加了访问延迟初始化字段的成本。
  延迟初始化(像许多“优化”一样)实际上会损害性能,具体取决于这些字段中最终需要初始化的部分、初始化的开销以及初始化后每个字段被访问的频率。
  也就是说,延迟初始化有它的用处。如果字段只在类实例的一部分上访问,并且初始化字段的开销很大,那么延迟初始化可能是值得的。确定的唯一方法是在延迟初始化和不延迟初始化的情况下度量类的性能。
  在存在多个线程的情况下,延迟初始化是很棘手的。如果两个或多个线程共享一个延迟初始化的字段,就必须使用某种形式的同步,否则可能导致严重的错误(item 78)。本项目中讨论的所有初始化技术都是线程安全的。
  在大多数情况下,正常初始化比延迟初始化更可取。下面是一个通常初始化的实例字段的典型声明。注意 final 的使用(item 17):

// Normal initialization of an instance field
private final FieldType field = computeFieldValue();

  如果你使用延迟初始化来打破初始化循环,使用 synchronized ,因为它是最简单,最清晰的替代:

// Lazy initialization of instance field - synchronized accessor
private FieldType field;
private synchronized FieldType getField() { 
  if (field == null)
    field = computeFieldValue(); 
  return field;
}

  这两种习惯用法(使用 synchronized 的常规初始化和延迟初始化) 在应用于静态字段时没有变化,只是在字段和访问器声明中添加了静态修饰符。
  如果需要在静态字段上使用延迟初始化以提高性能,请将延迟初始化的资源放在一个 holder 类中管理。这个技术利用了类在被使用之前不会被初始化的保证 [JLS, 12.4.1]。下面是它的样子:

// Lazy initialization holder class idiom for static fields
private static class FieldHolder {
  static final FieldType field = computeFieldValue();
}

private static FieldType getField() { 
  return FieldHolder.field; 
}

  当 getField 第一次被调用时,它读取 FieldHolder.field,导致初始化 FieldHolder 类。这种习惯用法的美妙之处在于,getField 方法没有同步,并且只执行字段访问,因此延迟初始化实际上不会增加访问成本。典型的虚拟机只会为了初始化类而同步字段访问。类初始化后,虚拟机会自动初始化静态成员,后续对字段的访问不需要涉及任何测试或同步。
  如果需要在实例字段上使用延迟初始化以提高性能,请使用双重检查习语。这个习惯用法避免了初始化后访问字段时的锁定开销(item 79)。这种习惯用法背后的思想是检查字段的值两次(因此得名 double-check):第一次不加锁定,然后,如果字段看起来没有初始化,第二次使用锁定。只有在第二个检查表明该字段未初始化时,调用才初始化该字段。因为字段初始化后就没有锁定,所以将该字段声明为 volatile 非常关键(item 78) :

// Double-check idiom for lazy initialization of instance fields
private volatile FieldType field;
private FieldType getField() {
  FieldType result = field;
  if (result == null) {  // First check (no locking)
    synchronized(this) {
      if (field == null) // Second check (with locking)
        field = result = computeFieldValue(); 
    }
  }
  return result; 
}

  这段代码可能看起来有点复杂。特别是,对局部变量 (result) 的需求可能不清楚。这个变量的作用是确保字段在已经初始化的情况下只被读取一次。虽然不是绝对必要的,但这可以提高性能,而且按照应用于低级并发编程的标准来看,这更优雅。在我的机器上,上面的方法比没有局部变量的版本快 1.4 倍。
  虽然您也可以对静态字段应用双重检查习惯用法,但是没有理由这样做:延迟初始化holder类是更好的选择。
  需要注意双重检查习语的两个变体。有时,您可能需要延迟初始化一个可以容忍重复初始化的实例字段。如果您遇到这种情况,您可以使用双重检查习语的变体来避免第二次检查。毫不奇怪,它被称为单检查习语。这是它的样子。注意,字段仍然声明为volatile:

// Single-check idiom - can cause repeated initialization! 
private volatile FieldType field;
private FieldType getField() { 
  FieldType result = field;
  if (result == null)
    field = result = computeFieldValue(); 
  return result;
}

  本项目中讨论的所有初始化技术都适用于原始类型和对象。当对数值基元字段应用双重检查或单次检查时,将根据0 (数值基元变量的默认值) 而不是 null 检查字段的值。
  如果您不关心是否每个线程都重新计算字段的值,并且字段的类型不是 long 或double,那么您可以选择在单检查习惯用法中从字段声明中删除 volatile 修饰符。这种变体被称为活泼的单勾选习语。在某些体系结构上,它加速了字段访问,但代价是额外的初始化(每个访问字段的线程最多进行一次初始化)。这绝对是一种新奇的技术,不适合日常使用。
  总之,您应该正常地初始化大多数字段,而不是延迟地初始化。如果为了实现性能目标或打破有害的初始化循环,必须延迟初始化字段,那么请使用适当的延迟初始化技术。例如字段,它是双重检查习语;对于静态字段,延迟初始化holder类。对于允许重复初始化的实例字段,还可以考虑单次检查习惯用法。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,240评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,328评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,182评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,121评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,135评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,093评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,013评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,854评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,295评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,513评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,398评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,989评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,636评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,657评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352