Implement Acquire/Release model by using volatile in java8
Asked Answered
P

2

5

In my case, I want to implement the Acquire/Release model in java8 with volatile.

So I write the code that uses a volatile shared variable I to guarantee the modifying of MAP could be seen by other threads.

public static volatile int I = 0;

public static final Map<String, String> MAP = new HashMap<>();

// run at Thread-1
public static void write(){
    MAP.put("test", "test");
    I++;                      // release
}

// run at Thead-2
public static void read(){
    int i = I;                // acquire
    MAP.get("test");          // want to see the modifying by write()
}

My question is that:

  1. Is the code synchronize correctly?
  2. Is it possible that JIT eliminates the unused local variable i so that the acquire operation is invalid?
Phenanthrene answered 29/1, 2021 at 9:23 Comment(8)
Why not use a ConcurrentHashMap and remove the whole volatile int I?Endora
Your can't do that with volatile only.Cariotta
@Endora what I focused on is the semantic of volatile, the object MAP could be another class.Elemental
@Phenanthrene volatile variables are never cached by a thread. But that doesn't mean that the access to MAP is then synchronized across all threads. You'd have to do that yourselvesEndora
@Endora as the happends-before order say If x and y are actions of the same thread and x comes before y in program order, then hb(x, y). and If an action x synchronizes-with a following action y, then we also have hb(x, y)., the I++ is synchronized with int i = I;, and If hb(x, y) and hb(y, z), then hb(x, z)..Elemental
Please check the following post about how acquire/release can be upgraded to sequential consistency (so volatile). shipilev.net/blog/2014/on-the-fence-with-dependenciesStamper
@Endora volatile variables are cached. A volatile variable prevents certain compiler optimizations and it add specific CPU memory fences. But apart from that; there is no conceptual difference between a normal variable and a volatile variable on a hardware level. The cache coherence makes sure that variables in caches do not get out of sync.Stamper
@Endora The only way to prevent a variable getting cached is to use some form of non temporal loads/stores. But this is a completely different game.Stamper
E
6

First note that ++ on a volatile variable is not atomic, hence, you can’t rely on its value in case of multiple updates.

As long as there is only a single update, it may be sufficient to check whether the update did happen, but it is crucial to perform the check. Otherwise, there is no guaranty that the (supposed to be acquire) volatile read is subsequent to the (supposed to be release) volatile update.

Just consider the following timing:

      Thread 1        Thread 2

    ┌            ┐    [ Read I    ]
    │ map update │    ┌           ┐    
    └            ┘    │ map query │
    [ Write I    ]    └           ┘

Here, the two threads use the map concurrently, which is hopelessly broken, while the acquire and release actions have no consequences, as the acquire is not subsequent to the release.

You can only rely on this relationship if you check the value you’ve read and proceed only when it is the expected value written by the other thread.


Since the whole construct would work only for a single update, you can use a boolean instead:

private static volatile boolean I = false;

private static final Map<String, String> MAP = new HashMap<>();

// run at Thread-1
public static void write(){
    MAP.put("test", "test");
    I = true;                      // release
}

// run at Thead-2
public static void read(){
    if(I) {                // acquire
        MAP.get("test");          // want to see the modifying by write()
    }
}

You can not use this for more than one update, as the thread wanting to perform a second update must ensure not to start updating the map before all threads reading the map after the first update have completed. But this information is not available at all with this approach.

Everyplace answered 29/1, 2021 at 10:45 Comment(3)
And how about the second question? Is it possible that the JIT eliminates int i = I;?Elemental
The JMM doesn’t tell what the JIT can do or not, rather it tells which outcomes are legal. Since the answer demonstrates that without checking the read value, there is a legal outcome where the volatile read has no effect, you can derive the conclusion that a JIT could eliminate a volatile read when the value is not used, as the outcome would be the same. Note that in theory the same applies if an optimizer could prove that there is no write to the variable. Then, the read also has no effect, so the JIT could eliminate it.Everyplace
Thanks for your answer, you really really help many times, and the code I showed is just a sample that discusses the semantic of volatile and the behavior of JIT but not practical code, maybe this question will misleading someone(I receive a downvote...), so I decided to delete the question.Elemental
O
2

In my opinion, you confuse terminology. volatile in java offers sequential-consistency, while release/acquire semantics do not. You can think about it as volatile being stronger than release/acquire. You can read the beginning of this answer, to understand what a potential difference between the two is.

Then, there is this subsequent word in the documentation and it's meaning:

A write to a volatile field happens-before every subsequent read of that same field.

What this means is that a ThreadA has to observe that write to the volatile field that ThreadB did. That means you need to check if the write was seen:

boolean flag = ...

ThreadA: flag = true; // this is the "release"

ThreadB: if(flag) { .... } // this is the "acquire"

Only when ThreadB enters the if statement (that is when the acquire happened), can you guarantee that everything that happened before the write (flag = true) will be visible in the if clause.

You can achieve the same thing, via a cheaper way with VarHandle::setRelease and VarHandle::getAcquire, but be careful, as this offers less guarantees as volatile, specifically it does not offer sequential consistency.

So your example is a bit flawed. You can simplify slightly your understanding of release/acquire by thinking that release is a write that a reader must observe, with special rules, like volatile or VarHandle::setRelease/getAcquire. You do not do neither of these. Your I++ (besides not being atomic) is not observed by anyone else, your int i = I is not checked to see if the value was really written.

Overmatter answered 29/1, 2021 at 14:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.