这出现了一个令人惊讶的转折。
可以相对确定的第一件事是,这种影响是由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
循环中的计数器即可。
for (int i = 0
但是,即使将它增加到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} &apos
...
[Verified Entry Point]
...
0x0000000002b33198: je 0x0000000002b33338
0x0000000002b3319e: mov $0x0,%esi
0x0000000002b331a3: jmpq 0x0000000002b332bc
0x0000000002b331a8: mov 0x178(%rdx),%edi
0x0000000002b331ae: shl $0x3,%rdi
0x0000000002b331b2: cmp (%rdi),%rax
...
0x0000000002b33207: je 0x0000000002b33359
0x0000000002b3320d: mov 0xc(%rdi),%ebx
0x0000000002b33210: inc %ebx
0x0000000002b33212: mov %ebx,0xc(%rdi)
...
0x0000000002b3326f: mov %ebx,0xc(%rdi)
...
我得承认,我对此并不是非常“理解”,但可以看到它执行了一个 getfield c
,以及一些(部分内联的?)increase
和decrease
方法的调用。
然而,最终编译版本的run
方法是这样的:
Decoding compiled method 0x0000000002b34590:
Code:
[Entry Point]
[Constants]
# {method} {0x00000000246d0f00} &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
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
0x0000000002b346ec: cmp 0x178(%rdx),%r12d
0x0000000002b346f3: je 0x0000000002b34701
0x0000000002b346f5: add $0x10,%rsp
0x0000000002b346f9: pop %rbp
0x0000000002b346fa: test %eax,-0x1a24700(%rip) # 0x0000000001110000
0x0000000002b34700: retq
0x0000000002b34701: mov %rdx,%rbp
0x0000000002b34704: mov $0xffffff86,%edx
0x0000000002b34709: xchg %ax,%ax
0x0000000002b3470b: callq 0x0000000002a657a0
0x0000000002b34710: int3
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
[Deopt Handler Code]
0x0000000002b34725: callq 0x0000000002b3472a
0x0000000002b3472a: subq $0x5,(%rsp)
0x0000000002b3472f: jmpq 0x0000000002a67200
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聪明地发现了对getTotal
方法的调用序列。
c.increase();
c.decrease();
本质上是一个无操作,因此,它只做了这个:什么也没做。
volatile
关键字,编译器几乎可以为所欲为。你的代码中没有任何指示需要将Counter
的更改可见于其他线程的内容。因此,我猜测JIT完全优化掉了这段代码。但这也不能保证是正确的行为。所以我不知道在这里有什么有用的答案。未定义的行为会导致奇怪的结果。 - Thiloincrease
和decrease
内联;第二步是优化掉后面跟着的++
以及--
。可能会接着消除现在空的循环。 - Jörg W Mittag