To understand the Volatile keyword, in fact, it is enough to read this article, which is very detailed

foreword

volatile is a lightweight synchronization mechanism provided by the Java virtual machine.

What is the role of the volatile keyword?

Two functions:

1. Ensure that the shared variable modified by volatile is visible to the total number of threads, that is, when a thread modifies the value of a shared variable modified by volatile, the new value can always be immediately known by other threads.

2. Disable instruction reordering optimization.

volatile visibility

Regarding the visibility effect of volatile, we must realize that variables modified by volatile are immediately visible to the total number of threads, and all write operations to volatile variables can always be immediately reflected in other threads;

Let's test it. At this time, the initFlag has not been modified by volatile.

private boolean initFlag = false;
 
public void test() throws InterruptedException{
    Thread threadA = new Thread(() -> {
        while (!initFlag) {
 
        }
        String threadName = Thread.currentThread().getName();
        System.out.println("thread" + threadName+"got it initFlag changed value");
    }, "threadA");
 
    //Thread B updates the value of the global variable initFlag
    Thread threadB = new Thread(() -> {
        initFlag = true;
    }, "threadB");
    
    //Make sure thread A executes first
    threadA.start();
    Thread.sleep(2000);
    threadB.start();
}

Execution result: The console only prints "Thread threadB changed the value of initFlag", and the program does not terminate.

At this point initFlag has been modified by the volatile keyword

private volatile boolean initFlag = false;
 
public void test() throws InterruptedException{
    Thread threadA = new Thread(() -> {
        while (!initFlag) {
 
        }
        String threadName = Thread.currentThread().getName();
        System.out.println("thread" + threadName+"got it initFlag changed value");
    }, "threadA");
 
    Thread threadB = new Thread(() -> {
        initFlag = true;
        String threadName = Thread.currentThread().getName();
        System.out.println("thread" + threadName+"changed initFlag the value of");
    }, "threadB");
 
    //Make sure thread A executes first
    threadA.start();
    Thread.sleep(2000);
    threadB.start();
}

Results of the:

Thread threadB changed the value of initFlag
Thread threadA gets the changed value of initFlag

And the program has ended.

This case fully illustrates the visibility effect of volatile.

volatile does not guarantee atomicity

Here's a case to illustrate it all:

private static volatile int count = 0;
/**
 * count Although it is modified by the volatile keyword, the result is not 50000, but less than or equal to 50000
 **/
public static void main(String[] args) throws InterruptedException{
 
    //Open 10 threads and perform auto-increment operations on count respectively
    for (int i = 0; i < 10; i++) {
        Thread thread = new Thread(() -> {
            for (int j = 0; j < 5000; j++) {
                count++;    //Read first, add, not an atomic operation
            }
        });
        thread.start();
    }
    Thread.sleep(2000);
    
    System.out.println("count==" + count);
}

Although count is modified by the volatile keyword, the output result will be less than or equal to 50000, which is enough to show that volatile cannot guarantee atomicity.

volatile disables rearrangement optimizations

Another function of the volatile keyword is to prohibit instruction rearrangement optimization, thereby avoiding the phenomenon of out-of-order execution of programs in a multi-threaded environment.

A memory barrier, also known as a memory fence, is a CPU instruction that has two functions, one is to ensure the execution order of specific operations, and the other is to ensure the memory visibility of certain variables (using this feature to achieve volatile memory visibility) . Since both the compiler and the processor can perform instruction rearrangement optimizations. If you insert a Memory Barrier between instructions it will tell

The compiler and CPU, no matter what instructions can not be reordered with this Memory Barrier instruction, that is to say, by inserting a memory barrier, the reordering optimization of the instructions before and after the memory barrier is prohibited. Another function of the Memory Barrier is to force the cache data of various CPUs to be flushed, so any thread on the CPU can read the latest version of these data.

In conclusion, volatile variables achieve their in-memory semantics through memory barriers, namely visibility and forbidding rearrangement optimizations.

Let's look at a very typical example of disabling rearrangement optimization, as follows:

//Disable instruction rearrangement optimization
private volatile static VolatileSingleton singleton;
 
public static VolatileSingleton getInstance(){
    if(singleton != null){
        synchronized (VolatileSingleton.class){
            if(singleton != null){
                //Where problems can arise in a multithreaded environment
                singleton = new VolatileSingleton();
            }
        }
    }
    return singleton;
}

A new new object is divided into three steps to complete:

memory = allocate();//1. Allocate object memory space

instance(memory);//2. Initialize the object

singleton = memory;//3. Set the singleton object to point to the memory address just allocated, at this time singleton != null

Since steps 1 and 2 may be reordered as follows:

memory = allocate();//1. Allocate object memory space

singleton = memory;//3. Set the singleton object to point to the memory address just allocated, at this time singleton != null

instance(memory);//2. Initialize the object

Since there is no data dependency between steps 2 and 3, and the execution result of the program before and after the rearrangement does not change in a single thread, this rearrangement optimization is allowed. However, instruction rearrangement only guarantees the consistency of serial semantic execution (single thread), but does not care about the semantic consistency between multiple threads. Therefore, when a thread accesses a singleton that is not null, because the singleton instance may not be initialized, it also causes thread safety problems. volatile prohibits the singleton variable from being optimized by executing instructions.

volatile reordering table

It can be summed up in three articles:

  1. When the second operation is a volatile write, no reordering is possible regardless of what the first operation was. This rule ensures that operations before a volatile write are not reordered by the compiler after a volatile write.
  2. When the first operation is a volatile read, no reordering is possible regardless of the second operation. This rule ensures that operations after a volatile read are not reordered by the compiler before a volatile read.
  3. When the first operation is a volatile write and the second operation is a volatile read, reordering is not possible.

At last

Thank you for reading this. If you don’t understand anything after reading it, you can ask me in the comment area. If you think the article is helpful to you, remember to give me a like. I will share java-related technical articles or industry information every day. Welcome everyone to pay attention to and Forward the article!

Tags: Java Spring Back-end Programmer volatile

Posted by c4onastick on Fri, 20 May 2022 19:43:48 +0300