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を答えとして与えるのは正しいですか?
あなたが言ったように、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)
これは驚くべき方向に進んだ。
最初に言えることは(比較的確実に)、その影響は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
increase
とdecrease
メソッドのコンパイル済みバージョンを調べましたが、明らかなものは何も見つかりませんでした。しかし、run
ここではその方法が原因のようです。最初、run
メソッドのアセンブリには期待されるコードが含まれていました(ここに最も関連性の高い部分のみを投稿します):
Decoding compiled method 0x0000000002b32fd0:
Code:
[Entry Point]
[Constants]
# {method} {0x00000000246d0f00} 'run' '()V' in 'CounterJitTest$Person' ... [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
が、(部分的にインライン化された?)メソッドincrease
とdecrease
メソッドのいくつかの呼び出しを実行していることがわかります。
ただし、メソッドの最終的なコンパイル済みバージョンrun
は次のとおりです。
Decoding compiled method 0x0000000002b34590:
Code:
[Entry Point]
[Constants]
# {method} {0x00000000246d0f00} 'run' '()V' in 'CounterJitTest$Person'
# [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はインライン化increase
とdecrease
メソッドを。同じ値をインクリメントおよびデクリメントするだけです。そして、インライン化した後、JITは、呼び出しのシーケンスが
c.increase();
c.decrease();
は本質的にノーオペレーションであり、したがって、まさにそれを行います:何もありません。
変数をインクリメントおよびデクリメントするマルチスレッドコードが結果として常に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
この同期されていないコードの実際の動作については何も想定できないことを知っています。
それは多くの要因に依存します、例えば:
このコードはスレッドセーフではないことをご存知でしょう。JVMの外部で何が起こるかを制御できないため、他のPCで再現可能であるか、異なる構成を使用しているか、同じマシンで同じ構成の同じマシンで再現可能なコードの動作を予測することはできません(CPUの負荷他のアプリケーション)。
追記:マイクロベンチマークには、一部のリソースがまだロードされていないという事実に関連する副作用があります。クラスので、あなたのコードで競合状態は、最初の反復で、より頻繁にすることができCounter
かつがPerson
まだロードされていない(また、最初の繰り返しの実行時間が非常に長く、他よりもあることに注意してください)。
Reba McEntire が息子の Shelby Blackstock と共有しているクリスマスの伝統について学びましょう。
メーガン・マークルとマライア・キャリーが自然な髪の上でどのように結合したかについて、メーガンの「アーキタイプ」ポッドキャストのエピソードで学びましょう.
ハリー王子が家族、特にチャールズ王とウィリアム王子との関係について望んでいると主張したある情報源を発見してください。
ワイノナ・ジャッドが、母親のナオミ・ジャッドが亡くなってから初めての感謝祭のお祝いを主催しているときに、彼女が今では家長であることをどのように認識したかを学びましょう.
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?
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!
私たちの読者は、何千ものOxyLEDのT-02モーションセンサーライトストリップを何年にもわたって購入してきましたが、充電するのが面倒だと感じた場合、新しいT-04は素晴らしいアップグレードのように見えます。T-02のように、 T-04は、付属の粘着ストリップを介して基本的にあらゆる表面に取り付けることができ、暗闇での動きを検出すると自動的に点灯します。
写真:AP大統領ドナルド・トランプは、連邦捜査局長官のジェームズ・コミーを解雇したばかりです。火曜日の声明で、ホワイトハウスは、トランプが「両方の副検事総長ロッドの明確な勧告に基づいて行動するコミーをオフィスから削除したと述べましたローゼンスタインと司法長官のジェフセッション。
ウィキメディア・コモンズ経由西部のローマ帝国の中央機関が崩壊し、5世紀の間に州が分裂し、独自の道を進んだとき、新しい王国が出現しました。今日、私たちはこれらの新しい政治単位を特定の野蛮人グループで特定する傾向があります:ガリア南西部の西ゴート族、ガリア北部のフランク人、英国のアングロサクソン人、北アフリカのヴァンダル人。
BodyRestore ユーカリ シャワー スチーマーは、Amazon で 11,000 を超える 5 つ星の評価を得ています。セルフケアが必要な人へのバレンタインデーのギフトとして、ホームスパ製品を贈りましょう。
多くのAmazonの買い物客がUmlo H6ハンドヘルド掃除機を推奨しており、現在スーパーセール中です. ハンドヘルド デバイスには HEPA フィルターが装備されており、複数のアタッチメントが付属しています。Amazonで75%オフのときにハンドヘルド掃除機を購入する
オクタヴィア・スペンサーは、ヘルプで一緒に共演するずっと前に、シシー・スペイセク主演の 1990 年の映画「ロング・ウォーク・ホーム」でインターンとして働いていました。
ジュリア・フォックスは、彼女のTikTokで共有された応答ビデオで、「本当に申し訳ありません。今、本当に年齢を示しています」と述べました。
人々にチャンスを与えることは、人生で少し遅すぎると私は信じています。寛大に。