Javaのforループでスレッドセーフの最適化はありますか?

33
Kidsunbo 2019-05-06 23:35.

2つのスレッドでカウンターを変更するコードスニペットがあります。アトミック変数やロックをコードに入れなかったため、スレッドセーフではありません。コードを1回だけ実行すると、期待どおりの結果が得られますが、数回実行したいので、コードをforループに入れます。そして問題は、最初または最初の2つのループだけが私が期待する結果を生成するということです。残りのループでは、結果は常に0であり、スレッドセーフのようです。そのような結果をもたらすJava仮想マシンの内部演算子はありますか?

ループの数を変更しようとしましたが、最初の1つまたは2つは常に期待どおりですが、ループの数に関係なく、他は0です。

カウンター:

private static class Counter {
    private int count;

    public void increase() {
        count++;
    }

    public void decrease() {
        count--;
    }

    public int getCount() {
        return count;
    }
}

人:

// This is just a thread to increase and decrease the counter for many times.
private static class Person extends Thread {
    private Counter c;

    public Person(Counter c) {
        this.c = c;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100000; i++) {
            c.increase();
            c.decrease();
        }
    }
}

主な方法:

public static void main(String[] args) throws InterruptedException {
    for (int i = 0; i < 10; i++) {
        Counter c = new Counter();
        Person p1 = new Person(c);
        Person p2 = new Person(c);
        p1.start();
        p2.start();
        p1.join();
        p2.join();
        System.out.println("run "+i+": "+c.getCount());        
   }
}

出力:

run 0: 243
run 1: 12
run 2: 0
run 3: 0
run 4: 0
run 5: 0
run 6: 0
run 7: 0
run 8: 0
run 9: 0

残りの結果が常に0である理由はわかりませんが、JVMの最適化に関するものだと思います。いくつかのループが実行されたときにJVMがコードを最適化し、残りのループを省略して常に0を答えとして与えるのは正しいですか?

3 answers

14
SirFartALot 2019-05-06 23:42.

あなたが言ったように、JVMはここで最適化されていると思います。

最適化がそこで行われることを明確に示す、タイミングを含むいくつかの出力を質問に追加しました。

public static void main(String[] args) throws InterruptedException {

    for (int i = 0; i < 10; i++) {
        final long startTime = System.currentTimeMillis();
        Counter c = new Counter();
        Person p1 = new Person(c);
        Person p2 = new Person(c);
        p1.start();
        p2.start();
        p1.join();
        p2.join();
        final long endTime = System.currentTimeMillis();
        System.out.println(String.format("run %s: %s (%s ms)", i, c.getCount(), endTime - startTime));        
   }
}

結果:

run 0: 1107 (8 ms)
run 1: 1 (1 ms)
run 2: 0 (2 ms)
run 3: 0 (0 ms)
run 4: 0 (0 ms)
run 5: 0 (0 ms)
run 6: 0 (1 ms)
run 7: 0 (0 ms)
run 8: 0 (0 ms)
run 9: 0 (0 ms)

プログラムの最初の反復には多くの時間が必要ですが、後の実行ではほとんど時間が使用されません。

この振る舞いの最適化を疑うのは正当なようです。

を使用してvolatile int count

run 0: 8680 (15 ms)
run 1: 6943 (12 ms)
run 2: 446 (7 ms)
run 3: -398 (7 ms)
run 4: 431 (8 ms)
run 5: -5489 (6 ms)
run 6: 237 (7 ms)
run 7: 122 (7 ms)
run 8: -87 (7 ms)
run 9: 112 (7 ms)
27
Marco13 2019-05-07 06:59.

これは驚くべき方向に進んだ。

最初に言えることは(比較的確実に)、その影響はJITによって引き起こされるということです。コードスニペットをこのMCVEに結合しました。

public class CounterJitTest
{
    private static class Counter
    {
        private int count;

        public void increase()
        {
            count++;
        }

        public void decrease()
        {
            count--;
        }

        public int getCount()
        {
            return count;
        }
    }

    private static class Person extends Thread
    {
        private Counter c;

        public Person(Counter c)
        {
            this.c = c;
        }

        @Override
        public void run()
        {
            for (int i = 0; i < 1000000; i++)
            {
                c.increase();
                c.decrease();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException
    {
        for (int i = 0; i < 10; i++)
        {
            Counter c = new Counter();
            Person p1 = new Person(c);
            Person p2 = new Person(c);
            p1.start();
            p2.start();
            p1.join();
            p2.join();
            System.out.println("run " + i + ": " + c.getCount());
        }
    }
}

でそれを実行する

java CounterJitTest

質問で言及された出力を引き起こします:

run 0: 6703
run 1: 178
run 2: 1716
run 3: 0
run 4: 0
run 5: 0
run 6: 0
run 7: 0
run 8: 0
run 9: 0

-Xint(解釈モード)でJITをオフにする、つまり、次のように開始する

java -Xint CounterJitTest

次の結果が発生します。

run 0: 38735
run 1: 53174
run 2: 86770
run 3: 27244
run 4: 61885
run 5: 1746
run 6: 32458
run 7: 52864
run 8: 75978
run 9: 22824

JITが実際にどのように深くダイブするためにはない、私は、生成されたアセンブリを見て持っている、のHotSpot VM逆アセンブラで全体を開始しました。ただし、実行時間は非常に速かったので、次のように考えました。for-loopのカウンターを増やすだけです。

for (int i = 0; i < 1000000; i++)

しかし、それを増やしても100000000、プログラムはすぐに終了します。それはすでに疑惑を引き起こしました。で分解を生成した後

java -server -XX:+UnlockDiagnosticVMOptions -XX:+TraceClassLoading -XX:+LogCompilation -XX:+PrintAssembly -XX:+PrintInlining CounterJitTest

increasedecreaseメソッドのコンパイル済みバージョンを調べましたが、明らかなものは何も見つかりませんでした。しかし、runここではその方法が原因のようです。最初、runメソッドのアセンブリには期待されるコードが含まれていました(ここに最も関連性の高い部分のみを投稿します):

Decoding compiled method 0x0000000002b32fd0:
Code:
[Entry Point]
[Constants]
  # {method} {0x00000000246d0f00} &apos;run&apos; &apos;()V&apos; in &apos;CounterJitTest$Person&apos; ... [Verified Entry Point] ... 0x0000000002b33198: je 0x0000000002b33338 ;*iconst_0 ; - CounterJitTest$Person::[email protected] (line 35)

  0x0000000002b3319e: mov    $0x0,%esi 0x0000000002b331a3: jmpq 0x0000000002b332bc ;*iload_1 ; - CounterJitTest$Person::[email protected] (line 35)

  0x0000000002b331a8: mov    0x178(%rdx),%edi   ; implicit exception: dispatches to 0x0000000002b3334f
  0x0000000002b331ae: shl    $0x3,%rdi ;*getfield c ; - CounterJitTest$Person::[email protected] (line 37)

  0x0000000002b331b2: cmp    (%rdi),%rax        ;*invokevirtual increase
            ; - CounterJitTest$Person::[email protected] (line 37) ; implicit exception: dispatches to 0x0000000002b33354 ... 0x0000000002b33207: je 0x0000000002b33359 0x0000000002b3320d: mov 0xc(%rdi),%ebx ;*getfield count ; - CounterJitTest$Counter::[email protected] (line 9)
            ; - CounterJitTest$Person::[email protected] (line 37) 0x0000000002b33210: inc %ebx 0x0000000002b33212: mov %ebx,0xc(%rdi) ;*putfield count ; - CounterJitTest$Counter::[email protected] (line 9)
            ; - CounterJitTest$Person::[email protected] (line 37) ... 0x0000000002b3326f: mov %ebx,0xc(%rdi) ;*putfield count ; - CounterJitTest$Counter::[email protected] (line 14)
            ; - CounterJitTest$Person::[email protected] (line 38)

  ...

確かに、私はこれを深く「理解」していませんgetfield cが、(部分的にインライン化された?)メソッドincreasedecreaseメソッドのいくつかの呼び出しを実行していることがわかります。

ただし、メソッドの最終的なコンパイル済みバージョンrunは次のとおりです。

Decoding compiled method 0x0000000002b34590:
Code:
[Entry Point]
[Constants]
  # {method} {0x00000000246d0f00} &apos;run&apos; &apos;()V&apos; in &apos;CounterJitTest$Person&apos;
  #           [sp+0x20]  (sp of caller)
  0x0000000002b346c0: mov    0x8(%rdx),%r10d
  0x0000000002b346c4: 
<writer thread='2060'/>
[Loaded java.lang.Shutdown from C:\Program Files\Java\jre1.8.0_131\lib\rt.jar]
<writer thread='5944'/>
shl    $0x3,%r10 0x0000000002b346c8: cmp %r10,%rax 0x0000000002b346cb: jne 0x0000000002a65f60 ; {runtime_call} 0x0000000002b346d1: data32 xchg %ax,%ax 0x0000000002b346d4: nopw 0x0(%rax,%rax,1) 0x0000000002b346da: nopw 0x0(%rax,%rax,1) [Verified Entry Point] 0x0000000002b346e0: mov %eax,-0x6000(%rsp) 0x0000000002b346e7: push %rbp 0x0000000002b346e8: sub $0x10,%rsp         ;*synchronization entry
            ; - CounterJitTest$Person::[email protected] (line 35) 0x0000000002b346ec: cmp 0x178(%rdx),%r12d 0x0000000002b346f3: je 0x0000000002b34701 0x0000000002b346f5: add $0x10,%rsp
  0x0000000002b346f9: pop    %rbp
  0x0000000002b346fa: test   %eax,-0x1a24700(%rip)        # 0x0000000001110000
            ;   {poll_return}
  0x0000000002b34700: retq   
  0x0000000002b34701: mov    %rdx,%rbp
  0x0000000002b34704: mov    $0xffffff86,%edx 0x0000000002b34709: xchg %ax,%ax 0x0000000002b3470b: callq 0x0000000002a657a0 ; OopMap{rbp=Oop off=80} ;*aload_0 ; - CounterJitTest$Person::[email protected] (line 37)
            ;   {runtime_call}
  0x0000000002b34710: int3                      ;*aload_0
            ; - CounterJitTest$Person::[email protected] (line 37) 0x0000000002b34711: hlt 0x0000000002b34712: hlt 0x0000000002b34713: hlt 0x0000000002b34714: hlt 0x0000000002b34715: hlt 0x0000000002b34716: hlt 0x0000000002b34717: hlt 0x0000000002b34718: hlt 0x0000000002b34719: hlt 0x0000000002b3471a: hlt 0x0000000002b3471b: hlt 0x0000000002b3471c: hlt 0x0000000002b3471d: hlt 0x0000000002b3471e: hlt 0x0000000002b3471f: hlt [Exception Handler] [Stub Code] 0x0000000002b34720: jmpq 0x0000000002a8c9e0 ; {no_reloc} [Deopt Handler Code] 0x0000000002b34725: callq 0x0000000002b3472a 0x0000000002b3472a: subq $0x5,(%rsp)
  0x0000000002b3472f: jmpq   0x0000000002a67200  ;   {runtime_call}
  0x0000000002b34734: hlt    
  0x0000000002b34735: hlt    
  0x0000000002b34736: hlt    
  0x0000000002b34737: hlt    

これはメソッドの完全なアセンブリです!そしてそれは...まあ、基本的に何もしません。

疑惑を確認するために、メソッドのインライン化を明示的に無効にincreaseしました。

java -XX:CompileCommand=dontinline,CounterJitTest$Counter.increase CounterJitTest

そして、出力は再び期待されたものでした:

run 0: 3497
run 1: -71826
run 2: -22080
run 3: -20893
run 4: -17
run 5: -87781
run 6: -11
run 7: -380
run 8: -43354
run 9: -29719

だから私の結論は:

JITはインライン化increasedecreaseメソッドを。同じ値をインクリメントおよびデクリメントするだけです。そして、インライン化した後、JITは、呼び出しのシーケンスが

c.increase();
c.decrease();

は本質的にノーオペレーションであり、したがって、まさにそれを行います:何もありません。

6
Davide Lorenzo MARINO 2019-05-06 23:51.

変数をインクリメントおよびデクリメントするマルチスレッドコードが結果として常に0になるとは限りません。

確実にできること:

  • Counterオブジェクトへのアクセスを同期します
  • Counterオブジェクト内で使用しますAtomicInteger

コードを実際にするcount++か、count--スレッドセーフではありません。内部的には、次のようなものと同等です。

load count     - load count from ram to the registry
increment count - increment by 1
store count    - save from the registry to ram

ただし、このコードは2つのスレッドによって呼び出された場合にこの動作をする可能性があります

    first                             second                           ram
    ----------                        --------                         ------
                                                                       count = 0
    load count
                                      load count
    (here count in registry == 0)     (here count in the second registry == 0)

    increment count       
                                      increment count

    (here count in registry == 1)     (here count in the second registry == 1)

    store count           
                                      store count
                                                                        count == 1

この同期されていないコードの実際の動作については何も想定できないこと知っています

それは多くの要因に依存します、例えば:

  • プロセッサの数
  • インクリメントおよびデクリメントコードの実行速度
  • プロセッサーの種類(I7マシンとAtomプロセッサーでは動作が異なる場合があります)
  • JVMの実装(OpenJDKまたはOracleJVMでは異なる動作をすることができます)
  • CPUの負荷
  • GCプロセスの実行の不在または存在

このコードはスレッドセーフではないことをご存知でしょう。JVMの外部で何が起こるかを制御できないため、他のPCで再現可能であるか、異なる構成を使用しているか、同じマシンで同じ構成の同じマシンで再現可能なコードの動作を予測することはできません(CPUの負荷他のアプリケーション)。


追記:マイクロベンチマークには、一部のリソースがまだロードされていないという事実に関連する副作用があります。クラスので、あなたのコードで競合状態は、最初の反復で、より頻繁にすることができCounterかつがPersonまだロードされていない(また、最初の繰り返しの実行時間が非常に長く、他よりもあることに注意してください)。

Related questions

MORE COOL STUFF

Reba McEntire は、彼女が息子の Shelby Blackstock と共有する「楽しい」クリスマスの伝統を明らかにしました:「私たちはたくさん笑います」

Reba McEntire は、彼女が息子の Shelby Blackstock と共有する「楽しい」クリスマスの伝統を明らかにしました:「私たちはたくさん笑います」

Reba McEntire が息子の Shelby Blackstock と共有しているクリスマスの伝統について学びましょう。

メーガン・マークルは、自然な髪のスタイリングをめぐってマライア・キャリーと結ばれました

メーガン・マークルは、自然な髪のスタイリングをめぐってマライア・キャリーと結ばれました

メーガン・マークルとマライア・キャリーが自然な髪の上でどのように結合したかについて、メーガンの「アーキタイプ」ポッドキャストのエピソードで学びましょう.

ハリー王子は家族との関係を修復できるという「希望を持っている」:「彼は父親と兄弟を愛している」

ハリー王子は家族との関係を修復できるという「希望を持っている」:「彼は父親と兄弟を愛している」

ハリー王子が家族、特にチャールズ王とウィリアム王子との関係について望んでいると主張したある情報源を発見してください。

ワイノナ・ジャッドは、パニックに陥った休暇の瞬間に、彼女がジャッド家の家長であることを認識しました

ワイノナ・ジャッドは、パニックに陥った休暇の瞬間に、彼女がジャッド家の家長であることを認識しました

ワイノナ・ジャッドが、母親のナオミ・ジャッドが亡くなってから初めての感謝祭のお祝いを主催しているときに、彼女が今では家長であることをどのように認識したかを学びましょう.

セントヘレナのジェイコブのはしごを登るのは、気弱な人向けではありません

セントヘレナのジェイコブのはしごを登るのは、気弱な人向けではありません

セント ヘレナ島のジェイコブズ ラダーは 699 段の真っ直ぐ上る階段で、頂上に到達すると証明書が発行されるほどの難易度です。

The Secrets of Airline Travel Quiz

The Secrets of Airline Travel Quiz

Air travel is far more than getting from point A to point B safely. How much do you know about the million little details that go into flying on airplanes?

Where in the World Are You? Take our GeoGuesser Quiz

Where in the World Are You? Take our GeoGuesser Quiz

The world is a huge place, yet some GeoGuessr players know locations in mere seconds. Are you one of GeoGuessr's gifted elite? Take our quiz to find out!

バイオニック読書はあなたをより速く読むことができますか?

バイオニック読書はあなたをより速く読むことができますか?

BionicReadingアプリの人気が爆発的に高まっています。しかし、それは本当にあなたを速読術にすることができますか?

OxyLEDの新しい15ドルのモーションセンシング常夜灯はどこにでも貼り付けられますが、充電が簡単です

OxyLEDの新しい15ドルのモーションセンシング常夜灯はどこにでも貼り付けられますが、充電が簡単です

私たちの読者は、何千ものOxyLEDのT-02モーションセンサーライトストリップを何年にもわたって購入してきましたが、充電するのが面倒だと感じた場合、新しいT-04は素晴らしいアップグレードのように見えます。T-02のように、 T-04は、付属の粘着ストリップを介して基本的にあらゆる表面に取り付けることができ、暗闇での動きを検出すると自動的に点灯します。

ハンソロ映画セットの写真は、新しいスーツと新しい乗り物を明らかにします

ハンソロ映画セットの写真は、新しいスーツと新しい乗り物を明らかにします

ユニバーサルは、フランケンシュタインの怪物を見つけるのに近いかもしれません。新しい撮影映像でアクマンの舞台裏をご覧ください。

ドナルド・トランプが解雇されたばかりのFBI長官ジェームズ・コミー

ドナルド・トランプが解雇されたばかりのFBI長官ジェームズ・コミー

写真:AP大統領ドナルド・トランプは、連邦捜査局長官のジェームズ・コミーを解雇したばかりです。火曜日の声明で、ホワイトハウスは、トランプが「両方の副検事総長ロッドの明確な勧告に基づいて行動するコミーをオフィスから削除したと述べましたローゼンスタインと司法長官のジェフセッション。

テオドリック大王は、過去の言語と政治的概念に隠された野蛮な将軍でした

テオドリック大王は、過去の言語と政治的概念に隠された野蛮な将軍でした

ウィキメディア・コモンズ経由西部のローマ帝国の中央機関が崩壊し、5世紀の間に州が分裂し、独自の道を進んだとき、新しい王国が出現しました。今日、私たちはこれらの新しい政治単位を特定の野蛮人グループで特定する傾向があります:ガリア南西部の西ゴート族、ガリア北部のフランク人、英国のアングロサクソン人、北アフリカのヴァンダル人。

バレンタインデーにユーカリのシャワースチーマーで「最高の睡眠」を贈りましょう。

バレンタインデーにユーカリのシャワースチーマーで「最高の睡眠」を贈りましょう。

BodyRestore ユーカリ シャワー スチーマーは、Amazon で 11,000 を超える 5 つ星の評価を得ています。セルフケアが必要な人へのバレンタインデーのギフトとして、ホームスパ製品を贈りましょう。

この「邪悪な吸引力」を備えたこの250ドルのハンドヘルド掃除機は、Amazonで75%オフになりました

この「邪悪な吸引力」を備えたこの250ドルのハンドヘルド掃除機は、Amazonで75%オフになりました

多くのAmazonの買い物客がUmlo H6ハンドヘルド掃除機を推奨しており、現在スーパーセール中です. ハンドヘルド デバイスには HEPA フィルターが装備されており、複数のアタッチメントが付属しています。Amazonで75%オフのときにハンドヘルド掃除機を購入する

オクタヴィア・スペンサー、「ザ・ヘルプ」共演者のシシー・スペイセクが17歳で映画のインターンをした後、彼女のことを「実際に」思い出したと語る

オクタヴィア・スペンサー、「ザ・ヘルプ」共演者のシシー・スペイセクが17歳で映画のインターンをした後、彼女のことを「実際に」思い出したと語る

オクタヴィア・スペンサーは、ヘルプで一緒に共演するずっと前に、シシー・スペイセク主演の 1990 年の映画「ロング・ウォーク・ホーム」でインターンとして働いていました。

ジュリア・フォックス、「マスカラ」がTikTokユーザーの性的暴行コードだったことを知らなかったことを謝罪

ジュリア・フォックス、「マスカラ」がTikTokユーザーの性的暴行コードだったことを知らなかったことを謝罪

ジュリア・フォックスは、彼女のTikTokで共有された応答ビデオで、「本当に申し訳ありません。今、本当に年齢を示しています」と述べました。

メリック・ガーランドはアメリカに失敗しましたか?

バイデン大統領の任期の半分以上です。メリック・ガーランドは何を待っていますか?

メリック・ガーランドはアメリカに失敗しましたか?

人々にチャンスを与えることは、人生で少し遅すぎると私は信じています。寛大に。

良いものと醜いもの: 2022

良いものと醜いもの: 2022

もうわからない。何が「ヒット」かを正確に判断することは、もはやほとんど不可能に思えます。

楽しみのために — 2022 年のトップの新しい音楽再生

楽しみのために — 2022 年のトップの新しい音楽再生

ついに!私の 2022 年のトップ ニューミュージック プレイへようこそ。私は毎年これを共有して、友達とつながります。

ヒーズ・オール・アイヴ・ガット

ヒーズ・オール・アイヴ・ガット

あなたの心をチェックしてください。私たちの心はしばしば迷います。

Language