33

私はこのブログ投稿を読んでいました。

そして、作者はマルチスレッド環境への侵入について話していhashCode()ました。String

持っていることによって:

public int hashCode() {
     int h = hash;
     if (h == 0) {
         int off = offset;
         char val[] = value;
         int len = count;

         for (int i = 0; i < len; i++) {
             h = 31*h + val[off++];
         }
         hash = h;
     }
     return h;
 }

変更:

public int hashCode() {
     if (hash == 0) {
         int off = offset;
         char val[] = value;
         int len = count;

         int h = 0;
         for (int i = 0; i < len; i++) {
             h = 31*h + val[off++];
         }
         hash = h;
     }
     return hash;
 }

著者が言っていることと私が引用していること:

「ここで行ったことは、追加の読み取りを追加することです。ハッシュの2回目の読み取りは、返される前に行われます。奇妙なことに、発生する可能性は低いですが、最初の読み取りでは、正しく計算されたハッシュ値を返すことができます。 2番目の読み取りは0を返すことができます!これは、モデルが操作の広範な並べ替えを許可するため、メモリモデルで許可されます。2番目の読み取りは、コード内で実際に移動できるため、プロセッサは最初の読み取りの前にそれを実行します!」

それで、コメントをさらに調べて、誰かがそれをに並べ替えることができると言います

int h = hash;
if (hash == 0) {
  ...
}
return h;

そんなことがあるものか?並べ替えには、プログラムステートメントを上下に移動するだけだと思いました。それはどのような規則に従っていますか?Googleで検索し、JSR133 FAQを読み、Java Concurrency in Practiceの本を確認しましたが、特に並べ替えについて理解するのに役立つ場所が見つからないようです。誰かが私を正しい方向に向けることができれば、私は本当にそれをいただければ幸いです。

Louisが「並べ替え」の意味を明確にしてくれたおかげで、私は「byteCode」の観点から考えていませんでした。

ただし、なぜ2番目の読み取りを前面に移動できるのかはまだわかりません。これは、それをいくらか「バイトコード」形式に変換するための私の素朴な試みです。

簡略化のために、ハッシュコードの計算に使用される操作はとして表されcalchash()ます。したがって、私はプログラムを次のように表現します。

if (hash == 0)  {       
    h = calchash();
    hash = h;
}
return hash;

そしてそれを「バイトコード」形式で表現しようとしています。

R1,R2,R3 are in the operands stack, or the registers
h is in the array of local variables

プログラム順:

if (hash == 0)  {       ---------- R1 = read hash from memory (1st read)
                        ---------- Compare (R1 == 0)
    h = calchash();     ---------- R2 = calchash()
                        ---------- h = R2 (Storing the R2 to local variable h)
    hash = h;           ---------- Hash = h (write to hash)
}
return hash             ---------- R3 = read hash from memory again(2nd read)
                        ---------- return R3

並べ替えられた変換(コメントに基づく私のバージョン):

                        ---------- R3 = read hash from memory (2nd read) *moved*
if (hash == 0)  {       ---------- R1 = read hash from memory (1st read)
                        ---------- Compare (R1 == 0)
    h = calchash();     ---------- R2 = calchash()
                        ---------- h = R2 (Storing the R2 to local variable h)
    hash = h;           ---------- hash = h (write to hash)
}
return hash             ---------- return R3

コメントをもう一度確認すると、著者がこれに答えていることがわかりました。

並べ替えられた変換(ブログから)

r1 = hash;
if (hash == 0) {
  r1 = hash = // calculate hash
}
return r1;

このケースは実際には単一のスレッドで機能しますが、複数のスレッドで失敗する可能性があります。

JVMはに基づいて単純化を行っているようです

h = hash and it simplifies the use of R1, R2, R3 to single R1

したがって、JVMは命令を並べ替えるだけでなく、使用されるレジスタの量を減らすようにも見えます。

4

4 に答える 4

14

変更したコード:

public int hashCode() {
     if (hash == 0) { // (1)
         int off = offset;
         char val[] = value;
         int len = count;

         int h = 0;
         for (int i = 0; i < len; i++) {
             h = 31*h + val[off++];
         }
         hash = h;
     }
     return hash; // (2)
 }

(1)と(2)は並べ替えることができます:(1)はnull以外の値を読み取ることができ、(2)は0を読み取ることができます。計算はローカル変数で行われるため、Stringクラスの実際の実装では発生しません。また、戻り値はそのローカル変数でもあり、定義上、スレッドセーフです。

問題は、Javaメモリモデルは、共有変数(hash)が適切な同期なしでアクセスされた場合に保証を提供しないことです。特に、すべての実行が順次一貫性があることを保証するものではありません。揮発hash性であった場合、変更されたコードに問題はありません。

ps:そのブログの作者は、JLSの第17章(Javaメモリモデル)の作者の1人だと思います-とにかく彼を信じる傾向があります;-)


アップデート

さまざまな編集/コメントに続いて、これら2つの方法でバイトコードを詳しく見ていきましょう(単純にするために、ハッシュコードは常に1であると想定しています)。

public int hashcode_shared() {
    if (hash == 0) { hash = 1; }
    return hash;
}

public int hashcode_local() {
    int h = hash;
    if (h == 0) { hash = h = 1; }
    return h;
}

私のマシンのJavaコンパイラは、次のバイトコードを生成します。

public int hashcode_shared();
   0: aload_0                           //read this
   1: getfield      #6                  //read hash (r1)
   4: ifne          12                  //compare r1 with 0
   7: aload_0                           //read this
   8: iconst_1                          //constant 1
   9: putfield      #6                  //put 1 into hash (w1)
  12: aload_0                           //read this
  13: getfield      #6                  //read hash (r2)
  16: ireturn                           //return r2

public int hashcode_local();
   0: aload_0                           //read this
   1: getfield      #6                  //read hash (r1)
   4: istore_1                          //store r1 in local variable h
   5: iload_1                           //read h
   6: ifne          16                  //compare h with 0
   9: aload_0                           //read this
  10: iconst_1                          //constant 1
  11: dup                               //constant again
  12: istore_1                          //store 1 into h
  13: putfield      #6                  //store 1 into hash (w1)
  16: iload_1                           //read h
  17: ireturn                           //return h

最初の例では、共有変数の2つの読み取りがありますhash:r1とr2。上記のように、同期がなく、変数が共有されているため、Javaメモリモデルが適用され、コンパイラ/ JVMは2つの読み取りを並べ替えることができます。行#13は行#1*の前に​​挿入できます。

h2番目の例では、非共有変数でのスレッド内セマンティクスとプログラム順序保証のために、ローカル変数であるすべての操作が順次一貫している必要があります。

注:いつものように、並べ替えが許可されているという事実は、それが実行されることを意味するものではありません。現在のx86/ホットスポットの組み合わせでは実際には発生しない可能性があります。ただし、他の現在または将来のアーキテクチャ/JVMで発生する可能性があります。


*これはちょっとした近道です。実際に起こる可能性があるのは、コンパイラが次のように書き直す可能性があることhashcode_sharedです。

public int hashcode_shared() {
    int h = hash;
    if (hash != 0) return h;
    return (hash = 1);
}

このコードは、シングルスレッド環境では厳密に同等であるため(常に元のメソッドと同じ値を返します)、並べ替えが可能です。ただし、マルチスレッド環境ではhash、最初の2行の間に別のスレッドによって0から1に変更された場合、この並べ替えられたメソッドが誤って0を返すことは明らかです。

于 2012-09-23T17:39:49.323 に答える
1

注意すべき重要な点は、間違った答えを得る(0を返す)スレッドでは、ifステートメントの本体が実行されないことです。無視してください。何でもかまいません。

間違った読み取りスレッドは、不揮発性フィールドを2回読み取りますが、書き込みは行いません。つまり、2つの読み取りの順序について話しているだけです。主張は、これらが注文されていないということです。より複雑な状況では、エイリアシングが発生する可能性があり、コンパイラがこれが同じメモリ位置であるかどうかを確認するのは簡単ではありません。保守的なルートを取ると、最適化が妨げられる可能性があります。

于 2012-09-23T18:29:01.507 に答える
1

素人の言葉で言えば、この問題は読み取り(フェッチ)の並べ替えに関係していると思います。

各スレッドT1とT2は、すべての「入力」を取得して処理を実行することを望んでいます(厳密なvolatileマーキングなしで)。データを読み取る方法とタイミングに関してある程度の自由が与えられます。

悪いケース:

各スレッドは、(インスタンス)変数を2回読み取る必要があります。1回はをチェックしif、もう1回は戻り値をチェックします。ifT1が最初に読み取りを実行することを選択し、T2が最初に読み取りを実行することを選択するという引数について考えてみましょうreturn

これにより、 T1による更新とT2の2回目の読み取り(T2が条件のチェックに使用)の間hashに変数が(T1によって)変更される競合状態が作成されます。したがって、T2のテストはfalseであり、何も行わず、インスタンス変数0に対して(元々)読み取ったものを返します。hashif

修正されたケース:

各スレッドは(インスタンス)変数を1回読み取るだけで、すぐにそれを独自のローカル変数に格納します。これにより、読み取りの並べ替えの問題が発生しなくなります(読み取りが1つしかないため)。

于 2013-03-18T00:21:14.093 に答える
0

最初に悪いコード:

int hash = 0;

public int hashCode() {
     if (hash == 0) {
         int off = offset;
         char val[] = value;
         int len = count;

         int h = 0;
         for (int i = 0; i < len; i++) {
             h = 31*h + val[off++];
         }
         hash = h;
     }
     return hash;
 }

明らかに、これを次のように骨の折れる部分に減らすことができます。

int hash = 0;

public int hashCode() {
     if (hash == 0) {
         // Assume calculateHash does not return 0 and does not modify hash.
         hash = calculateHash();
     }
     return hash;
 }

現在、理論は、特定の方法で2番目のスレッドとインターレースされた1つのスレッドを並べ替えると、リターンがゼロになる可能性があることを示唆しています。私が想像できる唯一のシナリオは、次のようなものです。

// Pseudocode for thread 1 starts with <1>, thread 2 with <2>. 
// Rn are local registers.
public int hashCode() {
     <2> has not begun
     <1> load r1 with hash (==0) in preparation for return for when hash is != 0
     <2> begins execution - finds hash == 0 and starts the calculation
     <2> modifies hash to contain new value.
     <1> Check hash for zero - it is not so skip the contents of the if
     if (hash == 0) {
         // Assume calculateHash does not return 0 and does not modify hash.
         hash = calculateHash();
         <2> got here but at a time when <1> was up there ^^
     }
     <1> return r1 - supposedly containing a zero.
     return hash;
 }

しかし、私にとっては、同様の処理を適切なコードで行うことができます。

public int hashCode() {
     int h = hash;
     if (h == 0) {
         hash = h = calculateHash();
     }
     return h;
 }

その後

public int hashCode() {
     <2> has not begun
     <1> load r1 with hash (==0) in preparation for return for when h is != 0
     <2> begins execution - finds h == 0 and starts the calculation
     <2> modifies hash to contain new value.
     <1> load r2 with hash - from now on r2 is h
     int h = hash;
     <1> Check r2 for zero - it is not so skip the contents of the if
     if (h == 0) {
         hash = h = calculateHash();
     }
     <1> we know that when hash/h are non-zero it doesn't matter where we get our return from - both r1 and r2 must have the same value.
     <1> return r1 - supposedly containing a zero.
     return h;
 }

なぜ一方が現実の可能性であり、もう一方がそうではないのか理解できません。

于 2013-03-17T23:05:44.407 に答える