Kid*_*nbo 33 java multithreading thread-safety
我有一段代码,可以在两个线程中更改一个计数器。它不是线程安全的,因为我没有在代码中放置任何原子变量或锁。如果代码仅运行一次,它会给出正确的结果,但是我想运行几次,因此将代码放入for循环中。问题是只有前两个循环会生成我期望的结果。对于其余的循环,结果始终为0,这似乎是线程安全的。Java虚拟机中是否有任何内部运算符导致这种情况?
我尝试过更改循环数,而第一个或第二个总是我期望的,但是无论有多少个循环,其他的都是0。
计数器:
private static class Counter {
private int count;
public void increase() {
count++;
}
public void decrease() {
count--;
}
public int getCount() {
return count;
}
}
Run Code Online (Sandbox Code Playgroud)
人:
// 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();
}
}
}
Run Code Online (Sandbox Code Playgroud)
主要方法:
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 Code Online (Sandbox Code Playgroud)
输出:
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
Run Code Online (Sandbox Code Playgroud)
我不知道为什么其余结果总是为0。但是我想这与JVM的优化有关。完成某些循环后,JVM优化代码是否正确,并忽略其余循环并始终给出0作为答案,对吗?
Mar*_*o13 26
这发生了令人惊讶的转折。
一个人(相对确定)可以说的第一件事是影响是由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());
}
}
}
Run Code Online (Sandbox Code Playgroud)
运行它
java CounterJitTest
Run Code Online (Sandbox Code Playgroud)
导致问题中提到的输出:
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
Run Code Online (Sandbox Code Playgroud)
使用-Xint(解释模式)关闭JIT ,即以
java -Xint CounterJitTest
Run Code Online (Sandbox Code Playgroud)
导致以下结果:
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
Run Code Online (Sandbox Code Playgroud)
为了更深入地了解JIT的实际功能,我从HotSpot反汇编程序VM中开始了整个工作,以了解生成的程序集。但是,执行时间是如此之快,以至于我以为:好吧,我只是在for-loop中增加计数器:
for (int i = 0; i < 1000000; i++)
Run Code Online (Sandbox Code Playgroud)
但是即使增加它也会100000000导致程序立即完成。那已经引起了怀疑。产生反汇编后
java -server -XX:+UnlockDiagnosticVMOptions -XX:+TraceClassLoading -XX:+LogCompilation -XX:+PrintAssembly -XX:+PrintInlining CounterJitTest
Run Code Online (Sandbox Code Playgroud)
我查看了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::run@0 (line 35)
0x0000000002b3319e: mov $0x0,%esi
0x0000000002b331a3: jmpq 0x0000000002b332bc ;*iload_1
; - CounterJitTest$Person::run@2 (line 35)
0x0000000002b331a8: mov 0x178(%rdx),%edi ; implicit exception: dispatches to 0x0000000002b3334f
0x0000000002b331ae: shl $0x3,%rdi ;*getfield c
; - CounterJitTest$Person::run@9 (line 37)
0x0000000002b331b2: cmp (%rdi),%rax ;*invokevirtual increase
; - CounterJitTest$Person::run@12 (line 37)
; implicit exception: dispatches to 0x0000000002b33354
...
0x0000000002b33207: je 0x0000000002b33359
0x0000000002b3320d: mov 0xc(%rdi),%ebx ;*getfield count
; - CounterJitTest$Counter::increase@2 (line 9)
; - CounterJitTest$Person::run@12 (line 37)
0x0000000002b33210: inc %ebx
0x0000000002b33212: mov %ebx,0xc(%rdi) ;*putfield count
; - CounterJitTest$Counter::increase@7 (line 9)
; - CounterJitTest$Person::run@12 (line 37)
...
0x0000000002b3326f: mov %ebx,0xc(%rdi) ;*putfield count
; - CounterJitTest$Counter::decrease@7 (line 14)
; - CounterJitTest$Person::run@19 (line 38)
...
Run Code Online (Sandbox Code Playgroud)
诚然,我并没有很“理解”这一点,但是可以看到它确实做了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::run@-1 (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::run@8 (line 37)
; {runtime_call}
0x0000000002b34710: int3 ;*aload_0
; - CounterJitTest$Person::run@8 (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
Run Code Online (Sandbox Code Playgroud)
这是方法的完整组装!它确实...很好,基本上什么也没有。
为了证实我的怀疑,我明确地禁止了的内联increase通过启动方法,
java -XX:CompileCommand=dontinline,CounterJitTest$Counter.increase CounterJitTest
Run Code Online (Sandbox Code Playgroud)
输出再次是预期的:
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
Run Code Online (Sandbox Code Playgroud)
所以我的结论是:
JIT内联increase和decrease方法。它们仅递增和递减相同的值。内联之后,JIT很聪明,可以弄清楚对
c.increase();
c.decrease();
Run Code Online (Sandbox Code Playgroud)
本质上是没有操作的,因此恰好做到了:什么也没有。
Sir*_*Lot 15
我认为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 Code Online (Sandbox Code Playgroud)
结果:
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)
Run Code Online (Sandbox Code Playgroud)
程序的第一次迭代需要很多时间,而在以后的执行中几乎没有时间使用。
似乎有理由怀疑这种行为的最佳化。
使用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)
Run Code Online (Sandbox Code Playgroud)
您无法确保递增和递减变量的多线程代码始终将结果赋予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
Run Code Online (Sandbox Code Playgroud)
但是,如果由两个线程调用,则此代码可能具有此行为
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
Run Code Online (Sandbox Code Playgroud)
知道您不能对这个未同步的代码的真实行为承担任何责任。
它取决于许多因素,例如:
您知道此代码是线程不安全的。您无法尝试预测该代码在其他PC上或使用不同配置或在具有相同配置的同一台机器上可重现的任何行为,因为您无法控制JVM外部发生的情况(CPU负载其他应用程序)。
附加说明:微基准测试的副作用与某些资源尚未加载有关。在你的代码中的竞争条件可以在第一迭代,因为类更频繁的Counter和Person尚未装载(注意,同样的执行时间对于第一次迭代是更长的时间比其他人)。