The King's Museum

ソフトウェアエンジニアのブログ。

【Effective Java】項目76:防御的に readObject を書く

ストリームからオブジェクトをデシリアライズする readObject メソッドは実質的に public コンストラクタとして機能します。 そのため、クラスのコンストラクタで検査している正当性や不変式を readObject にも実装する必要があります。

Period クラスの例

項目39では、開始日時と終了日時の組み合わせを示すための Period クラスを実装しました。 Period インスタンスは次の不変性があり、どんな Period インスタンスも次の条件を満たすことが保証されています。

start プロパティは end プロパティ以前の日時を示している

public class Period {
    private final Date start;
    private final Date end;

    /**
     * @param start 期間の開始
     * @param end   期間の終わり。開始より前であってはならない。
     * @throws IllegalArgumentException start が end の後の場合。
     * @throws NullPointerException     start か end が null の場合
     */
    public Period(Date start, Date end) {
        this.start = new Date(start.getTime());
        this.end = new Date(end.getTime());

        if (this.start.compareTo(this.end) > 0) {
            throw new IllegalArgumentException(this.start + " after " + this.end);
        }
    }

    public Date getStart() {
        return new Date(start.getTime());
    }

    public Date getEnd() {
        return new Date(end.getTime());
    }
}

Period クラスをシリアライズするためには単に Serializable インタフェースを実装するだけです。 しかし、その方法では Period クラスの不変性を破る不正な Period インスタンスを生成することが可能になってしまいます。

項目39 ではコンストラクタとアクセッサにおいて防御的コピーを実装し、クラスの不変式が破られないように努力しました。 この努力が readObject メソッドにも必要になります。

不変式のチェック

単に Serializable を implements しただけではバイトストリームを改変することによって、簡単に Period クラスの不変性を突破することができます。 シリアライズによってバイトストリーム化された Date のバイナリに直接手を加え、Date インスタンスの値を改変します。

この攻撃を防ぐためには readObject メソッドで不変式をチェックし、InvalidObjectException をスローするようにします。

// 不変式をチェックする readObject メソッド
private void readObject(ObjectInputStream stream)
        throws IOException, ClassNotFoundExcpetion {
    stream.defaultReadObject();

    if (start.compareTo(end) > 0) {
        throw new InvalidObjectException(start + " after " + end);
    }
}

防御的コピー

不変式のチェックによって不正な Period インスタンスの生成を防ぐことができます しかし、これでは防御が不完全です。

正当な Period インスタンスを含むバイトストリームのあとに、Period インスタンスの private な Date フィールドへの参照を追加したバイトストリームを作ると可変な Period インスタンスを生成できてしまいます。

具体的には次のようなコードになります。

public class MutablePeriod {
    public final Period period;

    public final Date start;
    public final Date end;

    public MutablePeriod() {
        try {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream out = new ObjectOutputStream(bos);
            // Period インスタンスの書き込み
            out.writeObject(new Period(new Date(), new Date()));
            // Period インスタンスの特定のプロパティへの参照を作成
            byte[] ref = {0x71, 0, 0x7e, 0, 5};
            bos.write(ref);
            ref[4] = 4;
            bos.write(ref);

            // 可変な Period インスタンスの生成
            ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
            period = (Period) in.readObject();
            start = (Date) in.readObject();
            end = (Date) in.readObject();
        } catch (IOException | ClassNotFoundException e) {
            throw new AssertionError(e);
        }
    }
}

この MutablePeriod に対して、次のようなコードを実行すると簡単に Period の start と end を書き換えることができます。

MutablePeriod period = new MutablePeriod();
period.start.setTime(1);
period.end.setTime(0);

これを防ぐため項目39で用いた防御的コピーを利用します。

private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
        stream.defaultReadObject();

        start = new Date(this.start.getTime());
        end = new Date(this.end.getTime());

        if (this.start.compareTo(this.end) > 0) {
            throw new InvalidObjectException(this.start + " after " + this.end);
        }
    }

正当性検査を行う前に防御的コピーが行われることに注意してください。

また、防御的コピーを行うと、フィールドを final にすることはできません。 これは望ましいものではないかもしれませんが、攻撃を許す状態にしておくよりはだいぶましです。

writeUnshared, readUnshared

このようなオブジェクト参照による攻撃への対策として Java 4 で writeUnshared と readUnshared が追加されています。 しかし、これらは項目77で解説する、高度な攻撃に対して脆弱性を持っているので利用しないでください。

基準

防御的に readObject を書くかどうかは、同様のコンストラクタを不変式のチェックなしで公開するかどうか?という視点で考えてください。 もし、そのようなコンストラクタを書くことに不安があるならば、防御的に readObject を書く必要があります。

readObject を書くことはパブリックなコンストラクタを追加していると考えてください。 引数のバイトストリームは正当な物とは限りませんし、実際にシリアライズされたインスタンスである保証はありません。

【Coursera】Learning How To Learn を受講した

ひさしぶりに Coursera を受講した。

Learning How To Learn という講義。

https://www.coursera.org/learn/learning-how-to-learn

脳科学や心理学をもとにした学習のやり方を教えてくれる講義。

具体的には、

  • 毎日少しづつ勉強した方が長期記憶に残りやすい
  • 脳には集中モードと発散モードがあり、集中して勉強するだけではなく発散モードもうまく使うべき
  • 集中するためにはポモドーロを使おう
  • 学習の結果を意識をすると脳の痛みの分野が反応するので、学習のプロセスに意識を置こう
  • イメージと結びつけると長期に記憶しやすくなる。部屋に配置するようなイメージで記憶しよう

など、なんとなくどこかで聞いたことあるようなテクニックがいくつか紹介されている。

特に脳の発散モード(diffuse mode)という考え方が自分にとって画期的で、最近は仕事で意識的に取り入れてる。

集中モードと発散モード

脳には集中モード(focused mode)と発散モード(diffuse mode)があり、学習においてどちらも重要な側面を担っている。 集中モードでは脳は局所的にニューロンを発火させる。 発散モードでは脳は遠くの離れたニューロン同士を発火させる。

集中モードでは特定の箇所のニューロン同士が激しく反応するため、特定の物事を深く考え理解するのに向いている。 ちょうどピンボールで釘が敷き詰められた箇所をボールが通って激しく動きまわっているようなイメージ。 一方、発散モードでは遠くのニューロン同士が緩く反応するので柔軟で新しい発想を生むのに向いている。 これはピンボールで釘が互いに離れた場所でピンボールがゆるく通過していくイメージ。

コーディングやデバッグでは基本的に脳は集中モードになっている。 しかし、一度詰まってしまうと、集中モードでいる限り新たな視点は得られないので時間の無駄になってしまう。 そこで、そうなってしまったら脳を発散モードにうつす努力をする。 具体的にはコンビニに出かけてジュースを買ったり10分ほど外を散歩したりする。

これが実際かなり効果がある。 画期的なアイディアで劇的に解決!ということはほとんどないのだが、「ああ、こっちの条件で挙動確認すればいいのか」と違う視点で問題を見ることができるようになる。そうして自席に戻って30分くらい作業しているとすすっと問題が解決していたりする。

最近はこの方法を積極的に使うことでコーディング、デバッグ、少し大きめのタスクであっても大ハマリすることなく仕事ができている。 まあ、人によっては勤務中に外に散歩したりできないかもしれないが、席を立ったりするだけでだいぶ違うと思う。

思い返せば CODE COMPLETE でも同じようなことが書いてあったし、大学時代の教授も同じことを言っていた。 確かバートランド・ラッセルも何かの著書で無意識の能力について言及していた(気がする)。 きっと、気づいている人は積極的に使っているテクニックなのだろう。

感想

というわけで、たった4週しかない講義だったけど、使った時間の割には得たものは大きかった。

本来は学部生向けの講義なのだろうけど、社会人でも十分に役立つと思う。 現代で仕事をしていくには、どんな分野であれ素早く新しいことを学習していくことが求められているだろうから。

しかし、Coursera の講義を受講するたびに『10年前の学生時代にこういう講義を受講したかったなぁ』と強く思うなぁ。

【Effective Java】項目75:カスタムシリアライズ形式の使用を検討する(後半)

前回の記事ではデフォルトシリアライズとカスタムシリアライズの概要について説明しました。

hjm333.hatenablog.com

本記事では、カスタムシリアライズ実装の注意事項について説明します。

StringList のカスタムシリアライズ実装を再掲します。

public class StringList implements Serializable {
    private transient int size = 0;
    private transient Entry head = null;

    private static class Entry implements Serializable {
        String data;
        Entry next;
        Entry previous;
    }

    /**
     * この{@code StringList}インスタンスをシリアライズする。
     *
     * @serialData リストのサイズ({@code int})を書き出して、
     * 適切な順番にすべての要素({@code String})が続くようにする
     */
    private void writeObject(ObjectOutputStream s) throws IOException {
        s.defaultWriteObject();
        s.writeInt(size);
        for (Entry e = head; e != null; e = e.next) {
            s.writeObject(e.data);
        }
    }

    private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
        s.defaultReadObject();
        int num = s.readInt();
        for (int i = 0; i < num; i++) {
            add((String) s.readObject());
        }

    }
}

注意点

transient フィールド

StringList のカスタムシリアライズ実装では private フィールドに transient 修飾子が指定されています。

transient 修飾子は、デフォルトのシリアライズ対象からそのフィールドを除外することを指定します。 すなわち、カスタムシリアライズ実装では StringList のすべての private フィールドは自動的にはシリアライズされなくなります。

デフォルトシリアライズを利用するかどうかに関わらず、defaultWriteObject を呼び出すと transient 指定されたフィールド以外のフィールドはすべてシリアライズされます。 無用なフィールドのシリアライズを避けるために、可能な限りフィールドは transient 指定するべきです。

特に transient を指定するべきフィールドとして、

  • データフィールドから値を計算できるフィールド(キャッシュなど)
  • ネイティブのデータ構造へのポインタを表すフィールド
  • 特定の JVM の1回の実行にひもづくフィールド

があります。

基本的にすべてのフィールドを transient 指定しようと考えてから、『このフィールドは transient を外そう』と考えるようにします。 この時、本当に transient でなくてよいかはよく確認してください。

transient フィールドのデフォルト値

transient 修飾子を指定した場合、デシリアライズされた際のフィールド値は、その型のデフォルト値になります。 オブジェクトならば null、boolean なら false、int ならば 0 です。

これらの値が受け入れられない場合、値を適切に設定する readObject メソッドを実装しなければなりません。 デシリアライズは言語外の仕組みを使うので、コンストラクタによる不変式の強制は利用できません。

defaultReadObject

StringList のフィールドはすべて transient 修飾子が指定されています。

すなわち、デフォルトシリアライズシリアライズされるフィールドはないはずです。 それにも関わらず writeObject 内で defaultWriteObject() を呼び出し、さらに readObject で defultReadObject() を呼んでいることに注意していください。

この呼び出しをすることで、後のリリースで StringList に transient ではないフィールドを追加することを可能にします。

もし、新しいバージョンで transient ではないプロパティが追加されて古いバージョンでデシリアライズした際、仮に defaultReadObject() を呼んでいないとしたら StreamCorruptedException が発生してしまいます。

@serialData

writeObject() は private メソッドにも関わらず、ドキュメンテーションコメントがつけられていることに注意してください。

writeObject() はシリアライズ形式を示しています。そして、シリアライズ形式は公開 API の一部です。 そのため、@serialData タグを用いてシリアライズされるデータの定義を書かなければなりません。

シリアライズが不適切な場合

前回述べたように StringList をデフォルトシリアライズにすることはいくつかの点で不適切です。 しかし、StringList のデフォルトシリアライズは『元のオブジェクトを正しくシリアライズし、デシリアライズによって正しく復元できる』という意味では正しいものです。

一方、デフォルトシリアライズを選ぶと、明確にエラーを引き起こすクラスがあります。

たとえばハッシュテーブルでは、デフォルトのシリアライズは機能しません。 なぜなら、キーとなるハッシュ値が、異なる JVM 間では違う可能性があります。 さらにいうとプログラムの実行ごとにハッシュ値が異なる可能性すらあります。

そのため、ハッシュテーブルにおけるデフォルトシリアライズによってハッシュ値シリアライズされると、デシリアライズした際に不変式が破られている可能性があります。

synchronized

もし、オブジェクトが同期によってスレッドセーフを実現しているならば、 writeObject メソッドは synchronized にする必要があります。

シリアルバージョン UID

Serializable を実装する場合、すべてのクラスで明示的なシリアルバージョン UID を定義してください。

シリアルバージョン UID は次のように宣言します。

private static final long serialVersionUID = randomLongValue;

明示的にシリアルバージョン UID を宣言すると、クラスのシリアライズに関する互換性を保つことができます。 これを宣言しない場合、シリアルバージョン UID はクラスの名前やフィールドを元にして自動生成されます。 すなわち、フィールド一つ追加しただけで、シリアルバージョン UID が変わってしまい、互換性が失われてしまいます。

また、明示的にシリアルバージョン UID を宣言しないと、実行時に UID を生成するためコストの高い計算が必要となります。

randomLongValue の値はどのような値でも問題ありませんが、serialvar ユティリティを実行すると値を生成できます。 最近の IDE には seiralVersionUID 生成機能が含まれています。

もし、クラスの古いバージョンにシリアルバージョン UID がついていない場合、新しいバージョンには古いバージョンのシリアルバージョン UID 自分で計算して、付け加える必要があります。

もし、異なるシリアルバージョン UID を持つクラスをデシリアライズしようとすると InvalidClassException が発生します。

【Effective Java】項目75:カスタムシリアライズ形式の使用を検討する(前半)

シリアライズする方法には次の二つの種類があります。

何の考慮もせずにデフォルトシリアライズ形式を用いることは、互換性やパフォーマンスの点で非常に危険です。 なるべくカスタムシリアライズ形式を用いるべきです。

デフォルトシリアライズ形式

デフォルトシリアライズを利用するには単に Serializable を実装します。 あとは何もしなくても、フレームワークが自動的にそのクラスの内部表現を効率的に符号化します。

すなわち、デフォルトシリアライズではクラスに含まれるすべてのプロパティが自動的にシリアライズされます。 そのため、本来はシリアライズするべきではない、実装上の都合やキャッシュなどのプロパティをすべてシリアライズしてしまいます。

たとえば、名前と年齢を持つ人物を表すクラスを考えてみます。 このクラスでデフォルトシリアライズを利用するには、単に次のように Serializable を実装するだけです。

public class Person implements Serializable {
    /**
     * 名前。非 null。
     * @serial
     */
    private String name;
    /**
     * 年齢。0以上。
     * @serial
     */
    private int age;

    public Person(String name, int age) {
        if (name == null) {
            throw new IllegalArgumentException("name = null");
        }
        if (age < 0) {
            throw new IllegalArgumentException("age: " + age + " < 0");
        }

        this.name = name;
        this.age = age;
    }
}

ここで、シリアライズに関するいくつか注意点があります。

@serial タグ

name と age のフィールドは private ですが Javadocドキュメンテーションコメントが含まれています。 private プロパヒットであっても、シリアライズされてしまえば公開された API として扱うため、ドキュメンテーションコメントをつけるべきです。

@serial タグを使えば Javadoc が「シリアライズ形式の文書化」に関する特別ページに、そのプロパティを掲載してくれます。

readObject メソッド

デフォルトシリアライズでは readObject メソッドの実装は必須ではありません。

しかし、不変式とセキュリティを保証するために readObject メソッドは必ず実装するべきです。

たとえば、Person では name と age には次の不変式があります。

  • name は null
  • age は 0 以上

もし、シリアライズされたファイルを操作して、これらの不変式を破壊するようなデータが仕込まれた場合、readObject がなければそのままデシリアライズされてしまうからです。 これに関する詳細については項目76で扱います。

カスタムシリアライズ形式

カスタムシリアライズを用いるためには Serializable を実装することに加えて readObject メソッドと writeObject メソッドを実装します。

readObject メソッドと writeObject メソッドで、開発者がシリアライズするべきプロパティを選びます。

これによって、単にクラスの物理表現(実装)がシリアライズするのではなく、オブジェクトの論理表現(仕様)をシリアライズすることができるようになります。

たとえば文字列のリスト示すクラスの StringList があるとします(これはあくまで例なので通常は List を使うべきです)。

public class StringList implements Serializable {
    private int size = 0;
    private Entry head = null;

    private static class Entry implements Serializable {
        String data;
        Entry next;
        Entry previous;
    }
    ...(省略)....
}

物理表現と論理表現

このクラスは論理的に表現すれば「文字列のリスト」ですが、物理的(実装的)に表現すると「文字列の双方向リンクリスト」と考えられます。 そして、デフォルトシリアライズを用いると物理表現である「文字列の双方リンクリスト」としてシリアライズされてしまいます。

これには次の4つの欠点があります。

  • 物理表現(実装)が公開 API となり、永久にサポートする必要がある
  • 多くの空間を消費する可能性がある
  • 多くの時間を消費する可能性がある
  • スタックオーバーフローを起こす可能性がある

一方、カスタムシリアライズを利用してクラスの論理表現をシリアライズすれば、これらの問題を回避し、よりシンプルにシリアライズすることができます。 論理的には StringList は論理的「リスト中の文字列数」と「文字列自身」を記憶しておけば十分だからです。

カスタムシリアライズを実装した StringList は次のようになります。

public class StringList implements Serializable {
    private transient int size = 0;
    private transient Entry head = null;

    private static class Entry implements Serializable {
        String data;
        Entry next;
        Entry previous;
    }

    /**
     * この{@code StringList}インスタンスをシリアライズする。
     *
     * @serialData リストのサイズ({@code int})を書き出して、
     * 適切な順番にすべての要素({@code String})が続くようにする
     */
    private void writeObject(ObjectOutputStream s) throws IOException {
        s.defaultWriteObject();
        s.writeInt(size);
        for (Entry e = head; e != null; e = e.next) {
            s.writeObject(e.data);
        }
    }

    private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
        s.defaultReadObject();
        int num = s.readInt();
        for (int i = 0; i < num; i++) {
            add((String) s.readObject());
        }

    }
}

このカスタムシリアライズの実装にはいくつか注意点があります。 次回の記事でその注意点について述べます。

感想

長いので分割。。。 これを含めてあと4つか~。

【Effective Java】項目74:Serializable を注意して実装する

項目74からはオブジェクトシリアライズ API について説明します

オブジェクトをバイト列として符号化することをシリアライズバイト列からオブジェクトに復号化することをデシリアライズと呼びます。

シリアライズされたオブジェクトは仮想マシン間やネットワークをまたいで転送できるようになります。 また、ファイルに保存できるようになります。

Serializable

Java でクラスをシリアライズ可能にするには Serializable インタフェースを implements します。

一般的に、コレクションクラスや値クラスは Serializable を実装しても問題ありません。 しかし、スレッドプールなどの処理をまとめたクラスは Serializable を実装するべきではありません。

やるべき作業は単に Serializable インタフェースを implements するだけですが、その簡単さとは裏腹にいくつか考慮しなければならない点があります。

柔軟性の低下

Serializable を実装したクラスを一旦リリースしてしまうと、その実装を変更することは容易ではありません。

クラスをシリアライズ可能にすると、そのシリアライズ形式がクラスの公開 API の一部となります。 既存のシリアライズ形式を永久にサポートし続ける必要がでてきます。

カスタムシリアライズ形式を設計せずにデフォルトのシリアライズ形式を利用すると、シリアライズ形式はクラスの内部表現と結びついてしまいます。 すなわち、デフォルトのシリアライズ形式では private インスタンスフィールドでさえも公開されてしまうことになります。

最も単純な問題例はシリアルバージョン UID と呼ばれるストリーム一意識別子に関する問題です。 Serializable を implements したクラスは必ずこの識別子を保持するようになります。 これを明示的に指定しない場合には次の情報からコンパイラが自動生成します。

  • クラス名
  • クラスが実装しているインタフェース名
  • public と protected のメンバ

もし、識別子を明示的に指定しない場合、単に便利なメソッドを一つ追加しただけでこの識別子は変更されてしまいます。 結果として、互換性が破壊されてしまいます。

セキュリティ

二つ目に、シリアライズ可能にすることでバグやセキュリティホールが入り込む可能性を増大させるという問題があります。

シリアライズ時のオブジェクトは言語外の仕組みを使い、通常のコンストラクタを迂回して生成されます。 すなわち、本来のコンストラクタによって保証される不変式がオブジェクト生成時には保証されません。

そのため、まずはデシリアライズ処理時に不変式を満たすように正しく実装する必要があります。 また、不変式を満たさない生成中のオブジェクト内部に外部からアクセスができないようにしなければなりません。

テスト負荷の増大

三つ目に、シリアライズ可能なクラスでは変更時のテストの工数が通常よりも増えます。

新たなリリースのインスタンスシリアライズし、古いリリースのデシリアライズ処理でインスタンスが復元できるを確認する必要があります。 また、逆に、古いリリースのインスタンスシリアライズし、新しいリリースのデシリアライズ処理で復元できるかも確認する必要があります。

これらのテストは単に新旧のインスタンス間でシリアライズ/デシリアライズできるかというバイナリ互換性に加えて、 動作が意図しているものかどうかというセマンティクス互換性も検査する必要があります。

これの負荷はカスタムシリアライズ形式を正しく設計することで軽減できますが、それを完全になくすことはできません。

継承と Serializable

継承するために設計されたクラス(項目17)とインタフェースでは Serializable を拡張するべきではありません。

継承して利用するために設計されたクラスが Serializable を implements している場合、サブクラスの実装にかなりの手間がかかります。

しかし、継承のために設計されたクラスがシリアライズ可能でない場合、シリアライズ可能なサブクラスを書くことは不可能かもしれません。 シリアライズ可能なサブクラスを書くためには、親クラスにアクセス可能なパラメータ無しコンストラクタが必要です。

内部クラスと static メンバークラス

内部クラスは Serializable を実装するべきではありません。 内部クラスはそれを包含するエンクロージングインスタンスに対する参照や、外部スコープからのローカル変数の値を保持するためのコンパイラが生成するフィールドを使用しています。 これらに対するデフォルトのシリアライズ形式は未定義で不明瞭で、動作は保証されません。

一方、static メンバークラスはエンクロージングインスタンスに対する参照などを持っていません。 そのため、自由に Serializable を実装できます。

【Effective Java】項目73:スレッドグループを避ける

スレッドグループはもはや利用するべきではありません。

スレッドグループのスレッド一覧を取得するための enumerate メソッドは、スレッドを格納する十分な大きさの配列が渡されない場合、勝手に一部のスレッドを無視します。 また、スレッドの数を取得する activeCount メソッドは、それを呼び出した後も正しいスレッド数を表しているとは限りません。

このように、スレッドグループの API が提供する多くの機能には欠陥があります。 もし、スレッドをまとめて管理したい場合には java.util.concurrent のエグゼキューターフレームワークを使うべきです。

setUncaughtExceptionHandler

スレッドグループのみが提供していた機能に「キャッチされない例外に対する制御を指定する」という機能があります。ThreadGroup.uncaughtException です。

Java 5 以降は Thread に setUncaughtExceptionHandler が追加されているため、こちらを使うべきです。

これで完全に ThreadGroup を使う必要はなくなりました。

【Effective Java】項目72:スレッドスケジューラに依存しない

プログラムの正しさやパフォーマンスをスレッドスケジューラーの動作に依存させてはいけません。

実行可能なスレッド数はプロセッサの数と比べて非常に大きくならないようにするべきです。 そうしておけば、スレッドスケジューラーがどのようなアルゴリズムを採用していてもプログラムは正しく動作します。

実行可能なスレッド数を少なくするためにはスレッド数を適切に制限するべきです。 エグゼキューターフレームワーク項目68)では、スレッド数を制限したスレッドプールを作成できます。

ビジーウェイト

特定の条件を満たすまでスレッドを待機させるためにビジーウェイトを使うべきではありません。 ビジーウェイトはプログラムを脆弱にし、プロセッサへの不可が増大します。

例えば次のようなビジーウェイトを用いた CountDownLatch はとても遅いです。

public class SlowCountdownLatch {
    private int count;

    public SlowCountdownLatch(int count) {
        if (count < 0) {
            throw new IllegalArgumentException(count + " < 0");
        }
        this.count = count;
    }

    public void await() {
        while (true) {
            synchronized (this) {
                if (count == 0) {
                    return;
                }
            }
        }
    }

    public synchronized void countDown() {
        if (count != 0) {
            count--;
        }
    }

}

Thread.yield()

Thread.yield() は他のスレッドに処理を実行させる機会を与えます。 しかし、機会を与えるだけで実際に他のスレッドの処理が実行されるとは限りません。

ある JVM 実装でパフォーマンスが改善する yield() 呼び出しが、他の実装では動作しないかもしれません。 実際、ある JVM 実装において yield() は何もしません。

Thread.yield() を利用する唯一の箇所はテストでした。 テストで yield を使うと実行パターンの状態空間の数が増え、バグをあぶり出すことができました。

しかし、前述したように JVM 実装によっては挙動が異なるため、普通は Thread.sleep(1) を使うべきです。 Thread.sleep(0) は実装によっては何もしないので、Thread.sleep(1) を使うべきです。

(c) The King's Museum