Visibility of Java Memory Model

Keywords: Java Programming

This blog series is a record summary of learning concurrent programming. Due to the large number of articles and the scattered time of writing, I arranged a directory post (transmission gate) for easy reference.

Concurrent programming series blog portal

Preface

As mentioned in the previous article, JMM is the embodiment of memory model specification in Java language. JMM ensures the atomicity, visibility and order of reading and writing shared variables in the multi-core CPU multithreading programming environment.

This article will talk about how JMM ensures the visibility of shared variable access.

What is visibility

Let's look at a simple piece of code to see what is visibility.

public class VolatileDemo {

    boolean started = false;

    public void startSystem(){
        System.out.println(Thread.currentThread().getName()+" begin to start system, time:"+System.currentTimeMillis());
        started = true;
        System.out.println(Thread.currentThread().getName()+" success to start system, time:"+System.currentTimeMillis());
    }

    public void checkStartes(){
        if (started){
            System.out.println("system is running, time:"+System.currentTimeMillis());
        }else {
            System.out.println("system is not running, time:"+System.currentTimeMillis());
        }
    }

    public static void main(String[] args) {
        VolatileDemo demo = new VolatileDemo();
        Thread startThread = new Thread(new Runnable() {
            @Override
            public void run() {
                demo.startSystem();
            }
        });
        startThread.setName("start-Thread");

        Thread checkThread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    demo.checkStartes();
                }
            }
        });
        checkThread.setName("check-Thread");
        startThread.start();
        checkThread.start();
    }

}

In the above column, one thread changes the state of started, and the other thread constantly detects the state of started. If it is true, the output system will start, if it is false, the output system will not start. After the start thread thread changes the status to true, can the check thread "see" the change immediately when it executes? The answer is that it doesn't have to be immediately visible. I have done a lot of tests here. In most cases, I can "sense" the change of started variable. But occasionally there are situations that are not perceived. Look at the following logging:

start-Thread begin to start system, time:1577079553515
start-Thread success to start system, time:1577079553516  
system is not running, time:1577079553516   ==>here start-Thread Thread has set state to true,however check-Thread Thread still not detected
system is running, time:1577079553516
system is running, time:1577079553516
system is running, time:1577079553516
system is running, time:1577079553516
system is running, time:1577079553516
system is running, time:1577079553516
system is running, time:1577079553517
system is running, time:1577079553517
system is running, time:1577079553517
system is running, time:1577079553517
system is running, time:1577079553517
system is running, time:1577079553517
system is running, time:1577079553517
system is running, time:1577079553519
system is running, time:1577079553519
system is running, time:1577079553519
system is running, time:1577079553519
system is running, time:1577079553519
system is running, time:1577079553519
system is running, time:1577079553519
system is running, time:1577079553519
system is running, time:1577079553519

The above phenomenon may be confusing. Why can check thread sometimes sense the change of state, and sometimes it can't? This phenomenon is the visibility problem in the multi-core CPU multithreaded programming environment.

Java Memory Model It is specified that all variables are stored in main memory, and each thread has its own working memory. The value saved by the thread in working memory is a copy of the value in main memory. All operations of the thread on variables must be carried out in working memory, instead of directly reading and writing to main memory. After the thread completes the operation on the variable, it will refresh the latest value of the variable back to main memory.

But when to refresh the latest value is random. So it is possible that a thread has updated a shared variable, but has not yet refreshed it back to main memory. Then other threads reading and writing this variable will not see the latest value. This is the visibility problem in multi CPU and multi thread programming environment. It is also the reason why the above code has problems.

JMM's guarantee for visibility

In the environment of multi CPU and multi thread programming, there will be visibility problems in reading and writing shared variables. But fortunately, JMM provides corresponding technical means to help us avoid these problems and make the program run correctly. JMM mainly provides the following means for visibility:

  • volatile keyword
  • synchronized keyword
  • Lock lock
  • CAS operation (atomic operation class)

volatile keyword

Using the volatile keyword to decorate a variable ensures variable visibility. So for the above code, we just need to modify the code to make the program run correctly.

private volatile boolean started = false;

Using volatile to decorate a shared variable can achieve the following effects:

  • Once the thread changes the copy of the shared variable, it will immediately refresh the latest value to main memory;
  • Once the thread modifies the copy of this shared variable, the copy value of this shared variable in other threads will fail. If other threads need to read and write this shared variable, they must reload it from the main memory.

So how does volatile achieve the above two effects? In fact, the underlying layer of volatile uses memory barriers to ensure visibility.

Memory barrier (English: memory barrier), also known as memory barrier, memory barrier, barrier instruction, etc., is a kind of synchronous barrier instruction. It is a synchronous point in the operation of random access to memory by CPU or compiler, so that all read and write operations before this point can be started after this point. In order to improve the performance of most modern computers, random execution is adopted, which makes the memory barrier necessary.

Semantically, all writes before the memory barrier need to be written to memory; reads after the memory barrier can obtain the results of writes before the synchronization barrier. Therefore, for sensitive blocks, memory barriers can be inserted after write operations and before read operations.
                 

Make a simple summary of the memory barrier:

  • Memory barrier is an instruction level synchronization point;
  • All write operations before the memory barrier must be immediately refreshed back to main memory;
  • The latest value must be read from the main memory after the memory barrier;
  • Where there is a memory barrier, instruction reordering is prohibited, that is, the code under the barrier cannot exchange the execution order with the code above the barrier, that is, when the memory barrier is executed, all the operations before it have been completed.

synchronized keyword

Using synchronized code blocks or synchronized methods can also ensure the visibility of shared variables. As long as we modify the above code as follows, we can get the correct execution result.

public synchronized void startSystem(){
    System.out.println(Thread.currentThread().getName()+" begin to start system, time:"+System.currentTimeMillis());
    value = 2;
    started = true;
    System.out.println(Thread.currentThread().getName()+" success to start system, time:"+System.currentTimeMillis());
}

public synchronized void checkStartes(){
    if (started){
        System.out.println("system is running, time:"+System.currentTimeMillis());
    }else {
        System.out.println("system is not running, time:"+System.currentTimeMillis());
    }
}

When the thread releases the lock, JMM will refresh the shared variables in the local memory corresponding to the thread into the main memory. When a thread acquires a lock, JMM will set the local memory corresponding to the thread to be invalid. Thus, the critical area code protected by the monitor must read the shared variables from the main memory. We found that locks have memory semantics consistent with volatile, so we can also achieve the visibility of shared variables by using synchronized.

Lock interface

Using Lock related implementation classes also ensures the visibility of shared variables. The implementation principle is similar to that of synchronized, so we will not repeat it here.

CAS mechanism (Atomic class)

Using atomic action classes also ensures visibility of shared variable actions. So we just need to revise the above code as follows.

private AtomicBoolean started = new AtomicBoolean(false);

CAS mechanism is used at the bottom of atomic operation class. In Java, CAS mechanism will get the latest value from main memory every time to compare, and the new value will be set to main memory only after it is consistent. And this whole operation is an atomic operation. So every time CAS operation gets the latest value in main memory, the value of each set will be immediately written to main memory.

Posted by jdesilva on Mon, 23 Dec 2019 00:49:56 -0800