15

先日、Howard Lewis Ship が「Things I Learned at Hacker Bed and Breakfast」というブログ エントリを投稿しました。箇条書きの 1 つは次のとおりです。

遅延初期化によって 1 回だけ割り当てられる Java インスタンス フィールドは、同期化または揮発性である必要はありません (フィールドに割り当てるスレッド間で競合状態を受け入れることができる限り)。これはリッチ・ヒッキーから

一見すると、これは、スレッド間のメモリ変更の可視性について一般に受け入れられている知識と矛盾しているように見えます。これが Java Concurrency in Practice 本または Java 言語仕様でカバーされている場合、私はそれを見逃しています。しかし、これは HLS が Brian Goetz が出席したイベントで Rich Hickey から入手したものなので、何かがあるに違いないと思われます。誰かがこの声明の背後にある論理を説明してもらえますか?

4

4 に答える 4

9

このステートメントは少し不可解に聞こえます。ただし、HLS は、インスタンス フィールドを遅延初期化し、複数のスレッドがこの初期化を複数回実行してもかまわない場合を指していると思います。例として、クラスのメソッドを
示すことができます:hashCode()String

private int hashCode;

public int hashCode() {
    int hash = hashCode;
    if (hash == 0) {
        if (count == 0) {
            return 0;
        }
        final int end = count + offset;
        final char[] chars = value;
        for (int i = offset; i < end; ++i) {
            hash = 31*hash + chars[i];
        }
        hashCode = hash;
    }
    return hash;
}

ご覧のとおり、hashCodeフィールド (計算された文字列ハッシュのキャッシュされた値を保持する) へのアクセスは同期されておらず、フィールドは として宣言されていませんvolatile。メソッドを呼び出すスレッドhashCode()は同じ値を受け取りますが、hashCodeフィールドは異なるスレッドによって複数回書き込まれる可能性があります。

この手法の有用性は限られています。IMHO主に例のような場合に使用できます。キャッシュされたプリミティブ/不変オブジェクトは、他の最終/不変フィールドから計算されますが、コンストラクターでの計算はやり過ぎです。

于 2012-06-15T13:57:18.780 に答える
6

うーん。これを読んだとき、技術的には正しくありませんが、実際にはいくつかの注意事項があります。一度安全に初期化でき、同期せずに複数のスレッドでアクセスできるのは最終フィールドのみです。

遅延初期化スレッドは、さまざまな方法で同期の問題に悩まされる可能性があります。たとえば、クラス自体が完全に初期化されずにクラスの参照がエクスポートされた場合、コンストラクターの競合状態が発生する可能性があります。

プリミティブ フィールドまたはオブジェクトがあるかどうかに大きく依存すると思います。複数のスレッドが初期化を行うことを気にしない場合に、複数回初期化できるプリミティブ フィールドは正常に機能します。ただしHashMap、この方法でのスタイルの初期化には問題がある場合があります。long一部のアーキテクチャの値でさえ、複数の操作で異なる単語を保存する可能性があるため、値の半分をエクスポートする可能性がありますがlong、メモリページを横切ることは決してないので、決して起こらないと思います。

アプリケーションにメモリバリア (ブロックやフィールドへのアクセス) があるどうかに大きく依存すると思います。悪魔は確かにここの詳細にあり、怠惰な初期化を行うコードは、1 つのコード セットを使用する 1 つのアーキテクチャでは正常に動作し、別のスレッド モデルやほとんど同期しないアプリケーションでは正常に動作する可能性があります。synchronizedvolatile


比較として、最終フィールドの良い部分を次に示します。

http://www.javamex.com/tutorials/synchronization_final.shtml

Java 5 の時点で、final キーワードの 1 つの特定の使用法は、同時実行の武器において非常に重要であり、見落とされがちな武器です。基本的に、final を使用して、オブジェクトを構築するときに、そのオブジェクトにアクセスする別のスレッドが、そのオブジェクトを部分的に構築された状態で認識しないようにすることができます。これは、オブジェクトの変数の属性として使用される場合、final がその定義の一部として次の重要な特性を持っているためです。

これで、フィールドが final とマークされていても、それがクラスであれば、クラスのフィールドを変更できます。これは別の問題であり、これにはまだ同期が必要です。

于 2012-06-15T13:47:13.310 に答える
4

私はその発言は真実ではないと思います。別のスレッドは部分的に初期化されたオブジェクトを見ることができるため、コンストラクターの実行が完了していなくても、参照が別のスレッドに表示される可能性があります。これについては、Java Concurrency in Practice のセクション 3.5.1 で説明されています。

public class Holder {

    private int n;

    public Holder (int n ) { this.n = n; }

    public void assertSanity() {
        if (n != n)
            throw new AssertionError("This statement is false.");
    }

}

このクラスはスレッドセーフではありません。

可視オブジェクトがimmutableである場合、問題ありません。 final フィールドのセマンティクスは、コンストラクターの実行が完了するまでそれらが表示されないことを意味するためです (セクション 3.5.2)。

于 2012-06-15T13:51:49.843 に答える
4

これは、いくつかの条件下では正常に機能します。

  • フィールドを複数回設定してみても問題ありません。
  • 個々のスレッドが異なる値を参照しても問題ありません。

多くの場合、ディスクからプロパティをロードするなど、変更されていないオブジェクトを作成する場合、短時間に複数のコピーを作成しても問題ありません。

private static Properties prop = null;

public static Properties getProperties() {
    if (prop == null) {
        prop = new Properties();
        try {
            prop.load(new FileReader("my.properties"));
        } catch (IOException e) {
            throw new AssertionError(e);
        }
    }
    return prop;
}

短期的には、これはロックを使用するよりも効率的ではありませんが、長期的にはより効率的です。(プロパティには独自のロックがありますが、アイデアはわかります;)

IMHO、すべてのケースで機能するソリューションではありません。

おそらく重要なのは、場合によっては、よりリラックスしたメモリ一貫性手法を使用できるということです。

于 2012-06-15T13:49:13.870 に答える