I have next sample of code:
class Shared {
int x;
int y;
void increment() {
x++;
y++;
}
void check() {
if (y > x) {
System.out.println("Ooops! y > x");
}
}
}
Looks clear? But main problem happens here when i am trying to increment and check in two threads:
Shared shared = new Shared();
Thread writer = new Thread(() -> {
for (int i = 0; i < N; i++) {
shared.increment();
}
});
Thread reader = new Thread(() -> {
for (int i = 0; i < N; i++) {
shared.check();
}
});
writer.start();
reader.start();
You can notice data race (instructions reordering in some cases?):
1. x++;
2. y++;
And now, i know about special VM flags, which can help me to print JIT compiler logs (-XX:+PrintCompilation
).
...
120 181 3 Shared::increment (21 bytes)
120 182 3 Shared::check (20 bytes)
120 183 4 Shared::increment (21 bytes)
120 184 4 Shared::check (20 bytes)
121 181 3 Shared::increment (21 bytes) made not entrant
121 182 3 Shared::check (20 bytes) made not entrant
121 185 n 0 java.lang.invoke.MethodHandle::linkToStatic(L)L (native) (static)
121 184 4 Shared::check (20 bytes) made not entrant
122 186 3 Shared::check (20 bytes)
122 187 n 0 java.lang.Object::clone (native)
122 188 4 Shared::check (20 bytes)
122 189 % 3 Main::lambda$main$0 @ 2 (19 bytes)
122 190 3 Main::lambda$main$0 (19 bytes)
123 186 3 Shared::check (20 bytes) made not entrant
...
OK, now i can see how compilation of increment method was processed:
120 181 3 Shared::increment (21 bytes)
120 183 4 Shared::increment (21 bytes)
121 181 3 Shared::increment (21 bytes) made not entrant
Do i understand correct, that reordering here is due of tiered compilation? Because increment()
- hot method, JIT compiler profiles this information and use C2
server compiler. And, as i think, reorder some instructions in such way, but in some cases optimization happens(made not entrant
). Or it's wrong?
Also, there are some another logs for compilation:
138 182 2 Shared::increment (21 bytes)
138 184 4 Shared::increment (21 bytes)
138 182 2 Shared::increment (21 bytes) made not entrant
This is not related to Tiered Compilation. The problem also happens without it. Let JVM compile just one method check
and see how it looks in C2-compiled code:
java -XX:-TieredCompilation \
-XX:CompileCommand=compileonly,Shared::check \
-XX:CompileCommand=print,Shared::check \
Shared
The output is
0x00000000031a4160: mov dword ptr [rsp+0ffffffffffffa000h],eax
0x00000000031a4167: push rbp
0x00000000031a4168: sub rsp,20h ;*synchronization entry
; - Shared::check@-1 (line 11)
(1) 0x00000000031a416c: mov r10d,dword ptr [rdx+0ch]
;*getfield x
; - Shared::check@5 (line 11)
(2) 0x00000000031a4170: mov r8d,dword ptr [rdx+10h] ;*getfield y
; - Shared::check@1 (line 11)
0x00000000031a4174: cmp r8d,r10d
0x00000000031a4177: jnle 31a4185h ;*if_icmple
; - Shared::check@8 (line 11)
0x00000000031a4179: add rsp,20h
0x00000000031a417d: pop rbp
0x00000000031a417e: test dword ptr [1020000h],eax
; {poll_return}
0x00000000031a4184: ret
As you can see, x
is loaded first (line 1), and y
is loaded after (line 2). Between these lines another thread may increase y
an aribrary number of times, thus making y
seem greater than x
.
In this particular case you've guessed about reordering of loads with respect to the original program order (in the bytecode getfield y
goes before getfield x
). However, as @Andreas mentioned, this is not the only reason why the program may break. Even if JIT compiler emits load(y)
before load(x)
, depending on the CPU architecture, it may happen that the first load gets a newer value, while the second load gets an older one, and this will be absolutely correct from JMM perspective.