🦔

Java における遅延初期化について

2021/03/28に公開

はじめに

  • Java における遅延初期化について Effective Java 第3版を読んで勉強しましたので、要約としてまとめます。

遅延初期化

  • 遅延初期化とは、そのフィールドが必要となるまで初期化を遅らせる行為のことを指します。遅延初期化を行う理由としては、初期化に多大なコストがかかるフィールドが存在した場合、遅延初期化を実装することによってパフォーマンスを向上させることができる点が挙げられます。

遅延初期化の実装方針

  • ただ遅延初期化は諸刃の剣とも呼ばれており、場合によっては実装することで返ってパフォーマンスが悪化することもあります。なぜならば、遅延初期化を実装するとフィールドの初期化コストを減少させる代わりに、そのフィールドへのアクセスコストを増加させてしまうからです。したがって実装方針としては、基本的には通常の初期化をまず検討し、フィールドへのアクセス頻度や初期化コストを考慮して遅延初期化を採用した方がパフォーマンスが向上するという判断ができる状況においては遅延初期化を検討するというのが望ましいようです。

実装方法

  • Java の遅延初期化に関しては static フィールドとインスタンスフィールドのどちらでも実装することが可能です。それぞれのフィールドに対する実装例を示します。

static フィールドの遅延初期化

  • まず static フィールドの遅延初期化の実装例を以下に示します。
staticフィールドに対する遅延初期化
private static class FieldHolder {
    static final FieldType field = computeFieldValue()
}

private static FieldType getField() { return FieldHolder.field; }
  • getField()メソッドを呼び出すと、メソッドは FieldHolder クラスの field にアクセスを行います。この時点で FieldHolder が初期化され、fieldcomputeFieldValue() が呼び出されることにより初期されることになります。この実装は遅延初期化ホルダー・クラス・イディオムと呼ばれています。この実装は同期処理を行っていませんのでアクセスコストは実質的には増加しません。同期処理を行っていないのは問題ではと考えるかもしれませんが、実際には JVM がクラスの初期化時にはフィールドのアクセスを同期しています。初期化が完了した後はクラスのフィールドに同期がかからないようにJVMがコードを修正します。

インスタンスフィールドの遅延初期化

  • 次にインスタンスフィールドの遅延初期化の実装例を以下に示します。
インスタンスフィールドに対する遅延初期化
private volatile FieldType field;  // (1)
    
private FieldType getField() {
    FieldType result = field;
    if (result != null)  // (2)
        return result;
    
    synchronized (this) {  
        if (field == null)  // (3)
            field = computeFieldValue();
        return field;
    }
}
  • この実装はパフォーマンス向上のために、できる限り同期処理のスコープを狭めているのが特徴で、二重チェックイディオムと呼ばれています。(1)で volatile 修飾子を付与しているのは、付与していないと各スレッドで field の値をキャッシュしてしまい、元の値の変更の検知を行うことができないためです。(2)と(3)でそれぞれ null チェックを実施しているのは、この区間で field の値が変更される可能性があるためです。したがって syncronized で同期を行った後に再び値が更新されているかのチェックを行っています。
  • また、インスタンスフィールドが複数回初期化することを許容できるのであれば、さらなるパフォーマンス向上のために以下のように実装することができます。
単一チェックイディオム
private volatile FieldType field;

private FieldType getField() {
    FieldType result = field;
    if (result == null) {
        field = result = computeFieldValue();
    }
    return result;
}
  • 上記の実装は、二重チェックイディオムにおける二重チェックの処理を省略したようなものとなっていて、単一チェックイディオムと呼ばれています。

まとめ

  • Javaにおける遅延初期化について学びました。ほとんどのフィールドに関しては遅延初期化をせずに普通に初期化を行うべきですが、パフォーマンスを向上させたいといったような理由がある場合は適切に遅延初期化を行いましょう。static フィールドに対しては遅延初期化ホルダー・クラス・イディオム、インスタンスフィールドに関しては二重チェックイディオム、もしくは単一チェックイディオムの使用を検討しましょう。

Discussion