web-dev-qa-db-ja.com

jdk1.6以上のHashMapがmulti = threadingで問題を引き起こすことを考えると、コードを修正するにはどうすればよいですか

最近、stackoverflowで質問をして、答えを見つけました。最初の質問は mutexやガベージコレクション以外のメカニズムがマルチスレッドを遅くする可能性があるJavaプログラム?

私は恐怖にHashMapがJDK1.6とJDK1.7の間で変更されたことを発見しました。現在、HashMapを作成するすべてのスレッドを同期させるコードブロックがあります。

JDK1.7.0_10のコード行は

 /**A randomizing value associated with this instance that is applied to hash code of  keys to make hash collisions harder to find.     */
transient final int hashSeed = Sun.misc.Hashing.randomHashSeed(this);

結局電話する

 protected int next(int bits) {
    long oldseed, nextseed;
    AtomicLong seed = this.seed;
    do {
        oldseed = seed.get();
        nextseed = (oldseed * multiplier + addend) & mask;
    } while (!seed.compareAndSet(oldseed, nextseed));
    return (int)(nextseed >>> (48 - bits));
 }    

他のJDKを見ると、これはJDK1.5.0_22またはJDK1.6.0_26には存在しません。

私のコードへの影響は大きいです。 64スレッドで実行すると、1スレッドで実行するよりもパフォーマンスが低下するようになります。 JStackは、ほとんどのスレッドがそのループでランダムにスピンすることにほとんどの時間を費やしていることを示しています。

だから私はいくつかのオプションがあるようです:

  • HashMapを使用しないようにコードを書き直しますが、似たようなものを使用します
  • どういうわけかrt.jarをいじり、その中のハッシュマップを置き換えます
  • クラスパスを何らかの方法で混乱させるため、各スレッドは独自のバージョンのHashMapを取得します

これらのパスのいずれかを開始する前に(すべて非常に時間がかかり、影響が大きい可能性があります)、明らかなトリックを逃したのではないかと思いました。スタックオーバーフローの人々は、どちらがより良いパスであるかを提案したり、新しいアイデアを特定したりすることができます。

助けてくれてありがとう

83
Stave Escura

私は、7u6に登場したパッチの最初の作者です。CR#7118743:ハッシュベースのマップを使用したStringの代替ハッシュshing。

HashSeedの初期化はボトルネックですが、Hash Mapインスタンスごとに1回しか発生しないため、問題になるとは予想していません。このコードをボトルネックにするには、1秒あたり数百または数千のハッシュマップを作成する必要があります。これは確かに典型的ではありません。 reallyアプリケーションがこれを行う正当な理由はありますか?これらのハッシュマップの有効期間は?

とにかく、おそらくcambeccで提​​案されているように、ランダムではなくThreadLocalRandomへの切り替えと、場合によっては遅延初期化のいくつかのバリアントを調査します。

EDIT 3

ボトルネックの修正がJDK7アップデートMercurialリポジトリにプッシュされました。

http://hg.openjdk.Java.net/jdk7u/jdk7u-dev/jdk/rev/b03bbdef3a88

この修正は今後の7u40リリースの一部であり、IcedTea 2.4リリースですでに利用可能です。

7u40のほぼ最終テストビルドは、ここから入手できます。

https://jdk7.Java.net/download.html

フィードバックはまだ歓迎されています。それを http://mail.openjdk.Java.net/mailman/listinfo/core-libs-dev に送信して、openJDK開発者に確実に表示されるようにしてください。

56
Mike Duigou

これは回避できる「バグ」のように見えます。新しい「代替ハッシュ」機能を無効にするプロパティがあります。

jdk.map.althashing.threshold = -1

ただし、代替ハッシュを無効にすると、ランダムハッシュシードの生成がオフにならないため、十分ではありません(実際にそうする必要があります)。したがって、Altハッシュをオフにしても、ハッシュマップのインスタンス化中にスレッドの競合が発生します。

これに対処する特に厄介な方法の1つは、ハッシュシード生成に使用されるRandomのインスタンスを、独自の非同期バージョンに強制的に置き換えることです。

// Create an instance of "Random" having no thread synchronization.
Random alwaysOne = new Random() {
    @Override
    protected int next(int bits) {
        return 1;
    }
};

// Get a handle to the static final field Sun.misc.Hashing.Holder.SEED_MAKER
Class<?> clazz = Class.forName("Sun.misc.Hashing$Holder");
Field field = clazz.getDeclaredField("SEED_MAKER");
field.setAccessible(true);

// Convince Java the field is not final.
Field modifiers = Field.class.getDeclaredField("modifiers");
modifiers.setAccessible(true);
modifiers.setInt(field, field.getModifiers() & ~Modifier.FINAL);

// Set our custom instance of Random into the field.
field.set(null, alwaysOne);

なぜこれを(おそらく)安全にするのですか?代替ハッシュが無効になっているため、ランダムハッシュシードが無視されます。したがって、Randomのインスタンスが実際にランダムでなくてもかまいません。このような厄介なハッキングではいつものように、注意して使用してください。

(静的な最終フィールドを設定するコードの https://stackoverflow.com/a/3301720/1899721 に感謝します)。

---編集---

FWIW、次のHashMapの変更により、altハッシュが無効になっている場合のスレッドの競合がなくなります。

-   transient final int hashSeed = Sun.misc.Hashing.randomHashSeed(this);
+   transient final int hashSeed;

...

         useAltHashing = Sun.misc.VM.isBooted() &&
                 (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
+        hashSeed = useAltHashing ? Sun.misc.Hashing.randomHashSeed(this) : 0;
         init();

ConcurrentHashMapなどにも同様のアプローチを使用できます。

30
cambecc

ビッグデータアプリケーションでは、レコードごとに一時的なHashMapを作成するアプリがたくさんあります。たとえば、このパーサーとシリアライザー。同期されていないコレクションクラスに同期を入れることは、本当に落とし穴です。私の意見では、これは受け入れられず、できるだけ早く修正する必要があります。 7u6で明らかに導入された変更(CR#7118743)は、同期またはアトミック操作を必要とせずに元に戻すか修正する必要があります。

どういうわけか、これはStringBufferとVectorおよびHashTableをJDK 1.1/1.2で同期させるという重大な間違いを思い起こさせます。人々はその間違いに対して何年も心から支払いました。その経験を繰り返す必要はありません。

3
user1951832

使用パターンが合理的であると仮定すると、独自のバージョンのHashmapを使用する必要があります。

そのコードはハッシュ衝突を引き起こしにくくし、攻撃者がパフォーマンスの問題を引き起こすのを防ぎます( details )-この問題が既に他の方法で処理されていると仮定すると、私はあなたとは思わない同期が必要です。ただし、同期を使用するかどうかは関係ありませんが、独自のバージョンのHashmapを使用して、JDKが提供するものにあまり依存しないようにしたいようです。

そのため、通常は似たようなことを書いてそれを指すか、JDKのクラスをオーバーライドします。後者を行うには、bootstrap classpathを-Xbootclasspath/p:パラメータ。ただし、そうすると「Java 2 Runtime Environmentバイナリコードライセンスに違反する」)( source )。

2
eis