Hello
it is an implementation from sun.misc.Unsafe.
public final int getAndAddInt(Object ptr, long offset, int value) {
int curr;
do {
curr = this.getIntVolatile(ptr, offset); (1)
} while(!this.compareAndSwapInt(ptr, offset, curr, curr + value)); (2)
return curr;}
Why there is
Unsafe.getIntVolatile()
called instead of
Unsafe.getInt()
here?
I am basically familiar with memory models, memory barriers etc., but, perhaps I don't see any something important.
getIntVolatile means here: ensure the order of execution: (1) -> (2)
It looks something like:
curr = read();
acquire();
CAS operation
Obviously, acquire() depends on CPU, for example on x86 it is empty, on ARM it is a memory barrier, etc.
My question/misunderstanding:
For my eye the order is ensured by data dependency between read of (ptr + offset) and CAS operation on it. So, I don't see a reason to worry about memory (re)ordering.
public final int getAndAddInt(Object ptr, long offset, int value) {
int curr;
do {
curr = this.getInt(ptr, offset);
} while(!this.compareAndSwapInt(ptr, offset, curr, curr + value));
return curr;
}
public final int getAndAddInt(Object ptr, long offset, int value) {
int curr = this.getInt(ptr, offset);
do {
} while(!this.compareAndSwapInt(ptr, offset, curr, curr + value));
return curr;
}
Ok. So the question below (ignoring other optimizations in the JVM that are specific to this method) is "If I were doing this myself in some other method, would this logic be valid if Unsafe.getIntVolatile() could be be replaced with Unsafe.getInt()?"The answer IMO is "no".The issue here is that unlike e.g. AtomicInteger.compareAndSet(), which is explicitly specified to include the behavior of a volatile read on the field involved, Unsafe.compareAndSwapInt() does not make any claims about exhibiting volatile read semantics. As a result, if you replace Unsafe.getIntVolatile() with Unsafe.getInt(), the resulting code:
public final int getAndAddInt(Object ptr, long offset, int value) {
int curr;
do {
curr = this.getInt(ptr, offset); (1)
} while(!this.compareAndSwapInt(ptr, offset, curr, curr + value)); (2)
return curr;
}
Can be validly transformed by the optimizer to:
public final int getAndAddInt(Object ptr, long offset, int value) {
int curr = this.getInt(ptr, offset); (1)
do {
} while(!this.compareAndSwapInt(ptr, offset, curr, curr + value)); (2)
return curr;
}
Because:(a) The optimizer can prove that if the compareAndSwapInt ever actually wrote to the field, the method would return and curr wouldn't be read again.
(b) Since the read of curr is not volatile, and the read in Unsafe.compareAndSwapInt() is not required to act like a volatile read, all the reads of curr can be reordered with the all the reads in the compareAndSwapInt() calls, which means that they can be folded together and hoisted out of the loop.If this valid optimization happened, the resulting code would get stuck in an infinite loop if another thread modified the field between the read of curr and the compareAndSwapInt call, and that is obviously not the intended behavior of getAndAddInt()...
The bytecode doesn't matter. It's not the javac compiler that will be doing the optimizations you should be worried about. It's the JIT compilers in the JVM. The javac-generated bytecode is only executed by the interpreter. The bytecode is eventually transformed to machine code by the JIT compiler, during which it will undergo aggressive optimization.
The "CPU" you should worry about and model in your mind is not x86, SPARC, or ARM. It's the JVM's execution engine and the JIT-generated machine code that does most of the actual execution. And that "CPU" will reorder the code more aggressively than any HW CPU ever would. The JIT's optimizing transformations include arbitrary and massive re-ordereing, reshaping, folding-together, and completely eliminating big parts off your apparent bytecode instructions. And the JIT will do all those as long as it can prove that the transformations are allowed.