Tips

书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code

注意,书中的有些代码里方法是基于Java 9 API中的,所以JDK 最好下载 JDK 9以上的版本。

83. 明智谨慎地使用延迟初始化

延迟初始化(Lazy initialization)是延迟属性初始化直到需要其值的行为。 如果不需要该值,则永远不会初始化该属性。 此技术适用于静态和实例属性。 虽然延迟初始化主要是一种优化,但它也可以用来打破类和实例初始化中的有害循环[Bloch05,Puzzle 51]。

与大多数优化一样,延迟初始化的最佳建议是“除非需要,否则不要这样做”(条目 67)。延迟初始化是一把双刃剑。它降低了初始化类或创建实例的成本,代价是增加了访问延迟初始化属性的成本。根据这些属性中最终需要初始化的部分、初始化它们的开销以及初始化后访问每个属性的频率,延迟初始化实际上会降低性能(就像许多“优化”一样)。

也就是说,延迟初始化有其用途。 如果仅在类的一小部分实例上访问属性,并且初始化属性的成本很高,则延迟初始化可能是值得的。 确切知道的唯一方法是使用和不使用延迟初始化来测量类的性能。

在存在多个线程的情况下,延迟初始化很棘手。如果两个或多个线程共享一个延迟初始化的属性,那么必须使用某种形式的同步,否则会导致严重的错误(条目 78)。本条目中讨论的所有初始化技术都是线程安全的。

在大多数情况下,正常初始化优于延迟初始化。 以下是通常初始化的实例属性的典型声明。 注意使用final修饰符(条目 17):

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

如果使用延迟初始化来破坏初始化循环,请使用同步访问器,因为它是最简单,最清晰的替代方法:

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

当应用于静态属性时,这两个习惯用法(正常初始化和使用同步访问器的延迟初始化)都不会更改,除了将static修饰符添加到属性和访问器声明。

如果需要在静态属性上使用延迟初始化来提高性能,请使用延迟初始化持有者类(lazy initialization holder class)的习惯用法。这个习惯用法保证了一个类知道被使用时才会被初始化[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方法不是同步的,只执行属性访问,因此延迟初始化几乎不会增加访问成本。 典型的虚拟机将仅同步属性访问以初始化类。 初始化类后,虚拟机会对代码进行修补,以便后续访问该属性不涉及任何测试或同步。

如果需要使用延迟初始化来提高实例属性的性能,请使用双重检查(double-check )习惯用法。这个习惯用法避免了初始化后访问属性时的锁定成本(条目 79)。这个习惯用法背后的思想是两次检查属性的值(因此得名double check):第一次没有锁定,然后,如果属性没有初始化,第二次使用锁定。只有当第二次检查指示属性未初始化时,才调用初始化属性。由于初始化属性后没有锁定,因此将属性声明为volatile非常重要(第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局部变量。 这个变量的作用是确保field属性在已经初始化的常见情况下只读一次。 虽然不是绝对必要,但这可以提高性能,并且通过应用于低级并发编程的标准更加优雅。 在我的机器上,上面的方法大约是没有局部变量的明显版本的1.4倍。

虽然也可以将双重检查用法应用于静态属性,但没有理由这样做:延迟初始化持有者类习惯用法(lazy initialization holder class idiom)是更好的选择。

双重检查习惯用法有两个变体值得注意。有时候,可能需要延迟初始化一个实例属性,该属性可以容忍重复初始化。如果你发现自己处于这种情况,可以使用双重检查的变体来避免第二个检查。毫无疑问,这就是所谓的“单一检查”习惯用法(single-check idiom)。它是这样的。注意,field仍然声明为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修饰符。 这种变体被称为生动的单一检查习惯用法(racy single-check idiom)。 它加速了某些体系结构上的属性访问,但代价是额外的初始化(直到访问该字段的线程执行一次初始化)。 这绝对是一种奇特的技术,不适合日常使用。

总之,应该正常初始化大多数属性,而不是延迟初始化。 如果必须延迟初始化属性以实现性能目标或打破有害的初始化循环,则使用适当的延迟初始化技术。 例如实例属性,使用双重检查习惯用法; 对于静态属性,使用延迟初始化持有者类习惯用法。 可以容忍重复初始化的属性,也可以考虑单一检查习惯用法。

Effective Java 第三版——83. 明智谨慎地使用延迟初始化的更多相关文章

  1. Effective Java 第三版——67. 明智谨慎地进行优化

    Tips 书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code 注意,书中的有些代码里方法是基于Java 9 API中的,所 ...

  2. Effective Java 第三版——66. 明智谨慎地使用本地方法

    Tips 书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code 注意,书中的有些代码里方法是基于Java 9 API中的,所 ...

  3. Effective Java 第三版——45. 明智审慎地使用Stream

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  4. Effective Java 第三版——52. 明智而审慎地使用重载

    Tips 书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code 注意,书中的有些代码里方法是基于Java 9 API中的,所 ...

  5. Effective Java 第三版—— 86. 非常谨慎地实现SERIALIZABLE接口

    Tips 书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code 注意,书中的有些代码里方法是基于Java 9 API中的,所 ...

  6. Effective Java 第三版——53. 明智而审慎地使用可变参数

    Tips 书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code 注意,书中的有些代码里方法是基于Java 9 API中的,所 ...

  7. Effective Java 第三版——55. 明智而审慎地返回Optional

    Tips 书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code 注意,书中的有些代码里方法是基于Java 9 API中的,所 ...

  8. 《Effective Java 第三版》目录汇总

    经过反复不断的拖延和坚持,所有条目已经翻译完成,供大家分享学习.时间有限,个别地方翻译得比较仓促,希望有疑虑的地方指出批评改正. 第一章简介 忽略 第二章 创建和销毁对象 1. 考虑使用静态工厂方法替 ...

  9. 《Effective Java 第三版》新条目介绍

    版权声明:本文为博主原创文章,可以随意转载,不过请加上原文链接. https://blog.csdn.net/u014717036/article/details/80588806前言 从去年的3月份 ...

随机推荐

  1. xss小结-从xss平台搭建到csp规则

    0x00前言 xss是跨站脚本攻击,利用嵌入js代码达到‘控制’对方浏览器的作用,测试的时候我们是用alert(1)弹窗,而做CTF也好,实际中的漏洞利用也好一般是用xss获取管理员的cookie 0 ...

  2. linux学习笔记 yum 在线管理软件包

    -y  如果yum在工作过程中需要使用者响应.这个参数可以直接回答yes #yum list 列出资源库中所有可安装或者可更新的rpm包 #yum perl 列出为perl的包 #yum perl* ...

  3. SpringBoot+Mybatis+MySql学习

    介绍一下SpringBoot整合mybatis,数据库选用的是mysql. 首先创建数据库 CREATE DATABASE test; 建表以及插入初始数据(sql是从navicat中导出的) SET ...

  4. 电脑用bat脚本给手机批量自动安装apk文件 autoInstall.bat

    ------创建这个文件autoInstall.bat 内容如下: @ECHO off @REM 将adb.exe添加到PATH中ECHO 初始化…@SET PATH=%PATH%;%CD%\Adb@ ...

  5. BZOJ1500: [NOI2005]维修数列 [splay序列操作]【学习笔记】

    以前写过这道题了,但我把以前的内容删掉了,因为现在感觉没法看 重写! 题意: 维护一个数列,支持插入一段数,删除一段数,修改一段数,翻转一段数,查询区间和,区间最大子序列 splay序列操作裸题 需要 ...

  6. 【DWM1000】 code 解密5一ACHOR 第一次回家Main 函数

    instance_run(); if((instance_data[0].monitor == 1) && ((portGetTickCnt() - instance_data[0]. ...

  7. 潭州课堂25班:Ph201805201 django 项目 第四十三课 后台 用户管理前后功能实现 (课堂笔记)

    用户的展示,编辑,删除, 把用户显示出来,用户名,员工(是,否), 超级用户(是, 否) 活跃状态,(非活跃示为删除) 在前台要显示该用户所属的用户组,在前台代码中是调用类的属性,所以在 user 的 ...

  8. 基于Systick系统时钟延时的LED闪烁灯

    1.回顾我们的51 单片机编程,当我们需要做系统延迟的时候,最常采用的一种方式就是使用for 循环的空语句等待来实现. 当然,在STM32 里面也可以这么实现.但是在STM32 的Cortex 内核里 ...

  9. 读《31天学会CRM项目开发》记录1 - 认识软件开发

    今天闲来无事,心中又对软件开发充满了向往和憧憬.一直认为实践是检验真知的唯一标准,也是快速提升的绝密方法,是巩固基础加深基础的好去处.故在JD上搜了下软件开发,看到了这本<31天学会CRM项目开 ...

  10. MySql中的约束

    mysql中的约束使用和oracle使用差别不大. 1.主键约束 如同人对应身份证,主键能够唯一地标识表中的一条记录,可以结合外键来定义数据表之间的关系. 主键约束要求主键列的数据唯一,并且不允许为空 ...