Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What's the difference between getVolatile and getAcquire?

Tags:

java

jls

What is the difference between getVolatile vs getAcquire when using e.g. AtomicInteger?

PS: those are related to

The source of a synchronizes-with edge is called a release, and the destination is called an acquire.

from https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.3

like image 384
adrhc Avatar asked Dec 28 '18 21:12

adrhc


2 Answers

It all comes back to how we want the optimisation of our code. Optimisation in terms of reordering code. Compiler might reorder to optimise. getAquire ensures that the instructions following it will not be executed before it. These instructions might be reordered but they will always be executed after the getAquire.

This works in combination with setRelease (for VarHandle) where setRelease ensures that what happens before it is not reordered to happen after it.

Example:

Thread1:

    var x = 1;
    var y = 2;
    var z = 3;

    A.setRelease(this, 10)

assignments of x, y and z will happen before A.setRelease but might be reordered themselves.

Thread 2:

if (A.getAquire(this) == 10) {
  // we know that x is 1, y is 2 and z = 3
}

This is a nice use case for concurrent program where you don't have to push volatility on everything but just need some instructions to be executed before another.

For getVolatile, the variable is treated just like any volatile variable in Java. No reordering or optimisation is happening.

This video is a nice to see to understand whats called the "memory ordering modes" being plain, opaque, release/acquire and volatile.

like image 53
Ali Ben Zarrouk Avatar answered Sep 20 '22 12:09

Ali Ben Zarrouk


One of the key differences between acquire/release and volatile (sequential consistency) can be demonstrated using Dekker's algorithm.

public void lock(int t) { 
    int other = 1-t; 
    flag[t]=1 
    while (flag[other] == 1) { 
        if (turn == other) { 
            flag[t]=0; 
            while (turn == other); 
            flag[t]=1 
        } 
    } 
} 

public void unlock(int t) { 
    turn = 1-t; 
    flag[t]=0 
}

So lets assume that the writing of the flag is done using a release store and the loading of the flag is done using an acquire-load, then we'll get the following ordering guarantees:

.. other loads/stores
[StoreStore][LoadStore]
flag[t]=1 // release-store
flag[other] // acquire-load
[LoadLoad][LoadStore]
.. other loads/stores

The problem is that the earlier write to flag[t] can be reordered with the later load of flag[other] and the consequence is that 2 threads could end up in the critical section.

The reason the earlier store and the later load to a different address can be reordered is 2 fold:

  1. the compiler could reorder it.
  2. modern CPU's have store buffers to hide the memory latency. Since stores are going to be made eventually anyway, there is no point on letting the CPU stall on a cache write miss.

To prevent this from happening a stronger memory model is needed. In this case we need sequential consistency since we do not want any reordering to take place. This can be realized by adding a [StoreLoad] between the store and the load.

.. other loads/stores
[StoreStore][LoadStore]
flag[t]=1 // release-store
[StoreLoad]
flag[other] // acquire-load
[LoadLoad][LoadStore]
.. other loads/stores

It depends on the ISA on which side this is done; e.g. on the X86 this is typically done on the writing side. E.g. using an MFENCE (there are other like XCHG that has an implicit lock or using a LOCK ADDL 0 to the stack pointer like the JVM typically does).

On the ARM this is done on the reading side. Instead of using a weaker load like an LDAPR, a LDAR is needed which will lead the LDAR to wait till the STLR's have been drained from the store buffer.

For a good read, check the following link: https://shipilev.net/blog/2014/on-the-fence-with-dependencies/

like image 42
pveentjer Avatar answered Sep 16 '22 12:09

pveentjer