82

私は最近、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は、ほとんどのスレッドがRandomのそのループでスピンすることにほとんどの時間を費やしていることを示しています。

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

  • HashMapを使用しないようにコードを書き直しますが、同様のものを使用します
  • どういうわけかrt.jarをいじって、その中のハッシュマップを置き換えます
  • どういうわけかクラスパスを台無しにするので、各スレッドは独自のバージョンのHashMapを取得します

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

助けてくれてありがとう

4

4 に答える 4

56

私は、7u6 に登場したパッチ CR#7118743 : Alternative Hashing for String with Hash-based Maps‌ の元の作成者です。

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

いずれにしても、Random ではなく ThreadLocalRandom への切り替えと、cambecc によって提案された遅延初期化の変種を調査する可能性があります。

編集3

ボトルネックの修正が JDK7 update 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 開発者が確実に認識できるようにします。

于 2012-12-27T19:58:56.713 に答える
30

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

jdk.map.althashing.threshold = -1

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

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

// 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、代替ハッシュが無効になっている場合のスレッドの競合が解消されます。

-   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などにも同様のアプローチを使用できます。

于 2012-12-23T16:22:54.897 に答える
3

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

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

于 2013-01-05T21:24:04.107 に答える
2

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

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

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

于 2012-12-23T13:55:09.680 に答える