Effective Java 第三版——83. 明智谨慎地使用延迟初始化

Tips
书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code
注意,书中的有些代码里方法是基于Java 9 API中的,所以JDK 最好下载 JDK 9以上的版本。

Effective Java 第三版——83. 明智谨慎地使用延迟初始化
  1. 明智谨慎地使用延迟初始化

延迟初始化(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)。 它加速了某些体系结构上的属性访问,但代价是额外的初始化(直到访问该字段的线程执行一次初始化)。 这绝对是一种奇特的技术,不适合日常使用。

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

Original: https://www.cnblogs.com/IcanFixIt/p/10645810.html
Author: 林本托
Title: Effective Java 第三版——83. 明智谨慎地使用延迟初始化

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/537090/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • ElasticSearch(三)SpringBoot 整合ES

    ElasticSearch(三)SpringBoot 整合ES 使用Java API 这种方式,官方已经明确表示在ES 7.0 版本中弃用 TransportClient 客户端,…

    Java 2023年6月5日
    0117
  • 1.17(设计模式)观察者模式

    观察者模式定义了对象间一对多的关系,当一个对象状态发生变化时,所有依赖于该对象的对象也将发生变化。 这就可以使用观察者模式,显示十进制数和二进制数依赖于具体的数字,当具体数字发生变…

    Java 2023年6月8日
    081
  • Java并发工具类

    在 JDK5 之后的并发包中提供的 CountDownLatch 也可以实现 join 的功能,并且比 join 的功能更多 2.CyclicBarrier CyclicBarri…

    Java 2023年5月29日
    090
  • MINIO使用

    1.作用 官网地址:https://docs.min.io/ 文件存储。文件对象的上传、下载和删除! 2.使用依赖 io.minio minio 8.4.3 3. 构建client…

    Java 2023年6月9日
    084
  • ThreadLocal用法

    ThreadLocal用法 ThreadLocal的实例代表了一个线程局部的变量,每条线程都只能看到自己的值,并不会意识到其它的线程中也存在该变量。ThreadLocal是为了解决…

    Java 2023年6月7日
    078
  • 程序员的职业素养

    一、专业主义 专业主义不但象征着荣誉和骄傲,而且明确意味着责任和义务,其精髓在于将公司利益视同个人利益,即担当责任。如不能为了交付时效而忽略测试环节,要为不完美承担责任(失误率永远…

    Java 2023年6月9日
    078
  • java 拦截器解决xss攻击

    一、xss攻击 XSS攻击通常指的是通过利用网页开发时留下的漏洞,通过巧妙的方法注入恶意指令代码到网页,使用户加载并执行攻击者恶意制造的网页程序。这些恶意网页程序通常是JavaSc…

    Java 2023年5月29日
    080
  • Java 获取Excel中的表单控件

    Excel中可通过【开发工具】菜单栏下插入表单控件,如文本框、单选按钮、复选框、组合框等等,插入后的控件可执行设置控件格式,如大小、是否锁定、位置、可选文字、数据源区域、单元格链接…

    Java 2023年5月29日
    084
  • (转)SpringBoot实现MultipartFile文件上传

    转:SpringBoot实现MultipartFile文件上传 – 云+社区 – 腾讯云 (tencent.com) 1、SpringBoot采用FileU…

    Java 2023年5月29日
    079
  • SpringBoot集成Mybatis-puls

    application.properties方式: application.yml方式: id类型:(可在实体类用注解@TableId(value=”id”…

    Java 2023年6月7日
    055
  • 高性能Java RPC框架Dubbo

    摘自《Java微服务分布式架构企业实战》 讲解了使用Spring Cloud来解决微服务应用程序开发过程中所遇到的一系列诸如客户端如何调用服务、服务与服务之间如何进行通信、服务如何…

    Java 2023年5月29日
    073
  • Maven 依赖调解源码解析(七):总结

    在本系列文章中,我们搭建了一个简单的多模块项目,以实验的形式,从源码角度解析了四种依赖调节原则。涉及到了传递依赖的两种调解原则、一种同文件内的覆盖原则,以及 dependencyM…

    Java 2023年6月16日
    055
  • 宿舍管理系统——单链表+结构体实现入住、退房和查询功能(C语言版)

    本程序的编译和运行环境如下(如果有运行方面的问题欢迎在评论区留言,也欢迎直接加QQ:2961439733,备注博客园或CSDN即可): 编辑工具:Dev-C++(版本:5.11.0…

    Java 2023年6月8日
    087
  • javaweb项目加载properties时找不到路径

    javase的写法: properties.load(new FileInputStream(“src\druid.properties”)); 报错: F…

    Java 2023年6月5日
    080
  • Java微服务:缓存穿透和缓存雪崩

    Java微服务:缓存穿透和缓存雪崩 缓存穿透 缓存是对数据库的一道保护墙,缓存穿透就是冲破了我们的保护墙。即 调用方传来的永远都是我们缓存中不存在的Key,这样每次都需要去数据库中…

    Java 2023年5月29日
    095
  • 【转载】JAVA基础:注解

    一、认识注解 注解(Annotation)很重要,未来的开发模式都是基于注解的,JPA是基于注解的,Spring2.5以上都是基于注解的,Hibernate3.x以后也是基于注解的…

    Java 2023年5月29日
    0114
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球