Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Implement Acquire/Release model by using volatile in java8

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?
like image 504
梁雨生 Avatar asked Jan 29 '21 09:01

梁雨生


Video Answer


2 Answers

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.

like image 180
Holger Avatar answered Oct 24 '22 14:10

Holger


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.

like image 28
Eugene Avatar answered Oct 24 '22 13:10

Eugene