I have a snippet of code that change a counter in two threads. It's not thread safe because I didn't put any atomic variable or lock in the code. It gives the right result as I expected if the code only run once, but I want to run it for several times, so I put the code into a for loop. And the question is that only the first or the first two loops will generate the result I expect. For the rest of the loops, the results are always 0, which seems to be thread safe. Is there any inner operator in Java Virtual Machine resulting such thing?
I have tried change the number of loops, and the first one or two are always what I expect, but the others are 0 no matter how many loops there are.
Counter:
private static class Counter { private int count; public void increase() { count++; } public void decrease() { count--; } public int getCount() { return count; } }
Person:
// 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(); } } }
Main method:
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()); } }
Output:
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
I don't know why the rest of the results are always 0. But I guess it's about the optimization of JVM. Is it right that the JVM optimizes the code when some loops have been done, and it omits the rest loops and always gives 0 as answer?
This code is thread-safe unless someone changes references to AtomicLongs.
The foreach operation itself is not thread safe. Say if you run a foreach loop to remove item from forward direction, it'll fail with "Collection was modified; enumeration operation may not execute." exception message.
Using Atomic Variable Using an atomic variable is another way to achieve thread-safety in java. When variables are shared by multiple threads, the atomic variable ensures that threads don't crash into each other.
This took a surprising turn.
The first thing that one can say (relatively sure) is that the effect is caused by the JIT. I combined the code snippets into this 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()); } } }
Running it with
java CounterJitTest
causes the output that was mentioned in the question:
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
Turning off the JIT with -Xint
(interpreted mode), that is, starting it as
java -Xint CounterJitTest
causes the following results:
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
In order to dive deeper into what the JIT actually does, I started the whole thing in a HotSpot disassembler VM, to have a look at the generated assembly. However, the execution time was so fast that I thought: Well, I'll just increase the counter in the for
-loop:
for (int i = 0; i < 1000000; i++)
But even increasing it to 100000000
caused the program to finish immediately. That already raised a suspicion. After generating the disassembly with
java -server -XX:+UnlockDiagnosticVMOptions -XX:+TraceClassLoading -XX:+LogCompilation -XX:+PrintAssembly -XX:+PrintInlining CounterJitTest
I looked at the compiled versions of the increase
and decrease
methods, but didn't find anything obvious. However, the run
method seemed to be the culprit here. Initially, the assembly of the run
method contained the expected code (only posting the most relevant parts here) :
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) ...
I don't deeply "understand" this, admittedly, but one can see that it does a getfield c
, and some invocations of the (partially inlined?) increase
and decrease
methods.
However, the final compiled version of the run
method is this:
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
This is the complete assembly of the method! And it does ... well, basically nothing.
To confirm my suspicion, I explicitly disabled the inlining of the increase
method, by starting with
java -XX:CompileCommand=dontinline,CounterJitTest$Counter.increase CounterJitTest
And the output was again the expected one:
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
So my conclusion is:
The JIT inlines the increase
and decrease
methods. They only increment and decrement the same value. And after inlining, the JIT is smart enough to figure out that the sequence of calls to
c.increase(); c.decrease();
is essentially a no-op, and hence, just does exactly that: Nothing.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With