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


83

私は最近stackoverflowで質問をし、その答えを見つけました。最初の質問は、ミューテックスまたはガベージコレクション以外のどのメカニズムマルチスレッド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を取得します

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

助けてくれてありがとう


2
何があなたにそんなに多くのハッシュマップを作成する必要がありますか?あなたは何をしようとしているのですか?
fge 2012

3
2つのコメント:1。ConcurrentHashMapはそれを使用していないようです-それは代替手段でしょうか?2.このコードは、マップの作成時にのみ呼び出されます。これは、高い競合の下で何百万ものハッシュマップを作成していることを意味します-それは本当に現実的な生産負荷を反映していますか?
assylias 2012

1
実際、ConcurrentHashMapもそのメソッドを使用します(oracle jdk 1.7_10)-しかし、明らかにopenJDK7は使用しません
assylias 2012

1
@assyliasここで最新バージョンを確認する必要があります。これは、そのようなコード行を備えています。
Marko Topolnik 2012

3
@StaveEscuraAtomicLongは、書き込み競合が少ないことに賭けてうまく機能します。書き込みの競合が多いため、定期的な排他ロックが必要です。同期されたHashMapファクトリを作成すると、これらのスレッドでマップのインスタンス化だけを行う場合を除い、おそらく改善が見られます。
Marko Topolnik 2012

回答:


56

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

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

とにかく、おそらくランダムではなくThreadLocalRandomへの切り替えと、cambeccによって提案されたレイジー初期化のいくつかのバリアントを調査します。

編集3

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

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

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

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

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

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


1
これを調べてくれてありがとう。はい、本当に多くのマップを作成する必要があります。アプリケーションは実際には非常に単純ですが、1秒間に数十万人がヒットする可能性があります。つまり、数百万のマップを非常に迅速に作成できます。もちろん、マップを使わないように書き直すこともできますが、開発コストが非常に高くなります。今のところ、リフレクションを使用して確率場をハックする計画は良さそうです
Stave Escura 2013年

2
マイク、短期的な修正の提案:ThreadLocalRandom(スレッドローカルストレージを台無しにするアプリケーションで独自の問題が発生する)を除けば、(時間、リスク、テストの点で)それほど簡単で安価ではないでしょう。 Hashing.Holder.SEED_MAKERを(たとえば)<num cores>ランダムインスタンスの配列にストライプ化し、呼び出し元のスレッドのIDを使用して%インデックスを付けますか?これにより、目立った副作用なしに、スレッドごとの競合が即座に緩和されます(解消されません)。
ホルガーHoffstätte

10
リクエスト率が高く、JSONを使用する@mduigou Webアプリケーションは、すべてではないにしてもほとんどのJSONライブラリがHashMapまたはLinkedHashMapsを使用してJSONオブジェクトを逆シリアル化するため、1秒あたり多数のHashMapを作成します。JSONを使用するWebアプリケーションは広く普及しており、HashMapの作成はアプリケーションによって制御されない場合があります(ただし、ライブラリアプリケーションの使用によって)。したがって、HashMapを作成するときにボトルネックがないのには正当な理由があると思います。
sbordet 2013年

3
@mduigouおそらく単純な緩和策は、CASを呼び出す前にoldSeedが同じかどうかを確認することです。この最適化(テストテストアンドセットまたはTTASとして知られている)は冗長に見えるかもしれませんが、CASが失敗することがすでにわかっている場合、CASは試行されないため、競合下でパフォーマンスに重大な影響を与える可能性があります。失敗したCASには、キャッシュラインのMESIステータスを無効に設定するという不幸な副作用があります。すべての関係者がメモリから値を再取得する必要があります。もちろん、Holgerによるシードのストライピングは優れた長期的な修正ですが、それでもTTAS最適化を使用する必要があります。
Jed Wesley-Smith

5
「数十万」ではなく「数十万」という意味ですか?-大きな違い
Michael Neale 2013年

30

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

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);

なぜこれを行うのが(おそらく)安全なのですか?altハッシュが無効になっているため、ランダムハッシュシードが無視されます。したがって、のインスタンスが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、などにも使用できます。


1
ありがとうございました。これは確かにハックですが、一時的に問題を解決します。それは確かに私が上で特定したリストのどれよりも良い解決策です。長期的には、とにかくより高速なHashMapで何かをしなければならないでしょう。これは、古いResourceBundleキャッシュがクリアできないという解決策を思い出させます。コードはほとんど同じです!
Stave Escura 2012

1
参考までに、この代替ハッシュ機能については、次のとおりです。レビューリクエストCR#7118743:ハッシュベースのマップを使用した文字列の代替ハッシュ。これは、murmur3ハッシュ関数の実装です。
cambecc 2012

3

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

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


2

使用パターンが妥当であると仮定すると、独自のバージョンのHashmapを使用することをお勧めします。

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

したがって、通常は似たようなものを記述してそれを指すか、JDKのクラスをオーバーライドします。後者を行うには、ブートストラップクラスパスを-Xbootclasspath/p:パラメーターでオーバーライドできます。ただし、そうすると、「Java 2ランタイム環境のバイナリコードライセンスに違反する」ことになります(ソース)。


あは。それが最適化のポイントだとは思いもしませんでした。非常に賢い。攻撃者に対する私の脅威モデルでは、このようにハッシュマップをいじることはありませんが、将来のためにこれを覚えておきます。最終的にHashMapを置き換えることについてのあなたの意見に同意します。おそらく、ファクトリオブジェクトまたはIOCコンテナを、それらを作成するすべてのクラスにスレッド化する必要があります。Cambeccからの回答は、私が長期的な解決策に取り組んでいる間、私を穴から抜け出させると思います
Stave Escura 2012
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.