Interview assault 44: what's the use of volatile?

volatile is an important part of Java Concurrent Programming and one of the common interview questions. Its main functions are to ensure the visibility of memory and prohibit instruction reordering. Let's look at these two functions in detail.

Memory visibility

When it comes to memory visibility, we have to mention the Java Memory Model. The Java Memory Model, abbreviated as JMM, is mainly used to shield the memory access differences of different hardware and operating systems, because there are certain differences in memory access under different hardware and operating systems. This difference will lead to different behaviors of the same code under different hardware and operating systems, The Java Memory Model is to solve this difference and unify the differences of the same code under different hardware and different operating systems.

The Java Memory Model stipulates that all variables (instance variables and static variables) must be stored in the main memory, and each thread will also have its own working memory. The working memory of the thread stores the variables used by the thread and a copy of the main memory, and the operation of the thread on the variables is carried out in the working memory. Threads cannot directly read and write variables in main memory, as shown in the following figure:

However, the Java memory model will bring a new problem, that is, the memory visibility problem. That is, when a thread modifies the value of a shared variable in the main memory, other threads cannot perceive that the value has been modified. It will always use the "old value" in its working memory. In this way, the execution result of the program does not meet our expectations. This is the memory visibility problem, Let's use the following code to illustrate this problem:

private static boolean flag = false;
public static void main(String[] args) {
    Thread t1 = new Thread(new Runnable() {
        @Override
        public void run() {
            while (!flag) {

            }
            System.out.println("Termination of execution");
        }
    });
    t1.start();
    Thread t2 = new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("set up flag=true");
            flag = true;
        }
    });
    t2.start();
}

The expected result of the above code is that after thread 1 executes for 1s, thread 2 modifies the flag variable to true, and then thread 1 terminates the execution. However, because thread 1 does not perceive the modification of the flag variable, that is, the memory visibility problem, thread 1 will execute forever. Finally, the result we see is as follows:

How to solve the above problems? You only need to modify the variable flag with volatile. The specific implementation code is as follows:

private volatile static boolean flag = false;
public static void main(String[] args) {
    Thread t1 = new Thread(new Runnable() {
        @Override
        public void run() {
            while (!flag) {

            }
            System.out.println("Termination of execution");
        }
    });
    t1.start();
    Thread t2 = new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("set up flag=true");
            flag = true;
        }
    });
    t2.start();
}

The execution results of the above procedures are shown in the figure below:

Prohibit instruction reordering

Instruction reordering refers to a means by which the compiler or CPU reorders instructions in order to optimize the execution performance of the program.

The original intention of instruction reordering is good, but in multi-threaded execution, if instruction reordering is executed, it may lead to program execution errors. The most typical problem of instruction reordering occurs in the singleton mode, such as the following problem code:

public class Singleton {
    private Singleton() {}
    private static Singleton instance = null;
    public static Singleton getInstance() {
        if (instance == null) { // ①
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton(); // ②
                }
            }
        }
        return instance;
    }
}

The above problem occurs in the line of code ② "instance = new Singleton();", This line of code seems to be just a process of creating objects, but its actual implementation is divided into the following three steps:

  1. Create memory space.
  2. Initializes the object Singleton in memory space.
  3. Assign the memory address to the instance object (after this step, instance is not equal to null).

If volatile is not added to this variable, thread 1 may perform instruction reordering when executing to ② of the above code, and rearrange the original execution order of 1, 2 and 3 to 1, 3 and 2. However, in special cases, after thread 1 completes step 3, if thread 2 executes to ① of the above code and judges that the instance object is not null, but thread 1 has not instantiated the object at this time, thread 2 will get an object that is "half" instantiated, resulting in program execution error. This is why volatile is added to private variables.

To turn the above singleton mode into a thread safe program, you need to add volatile modification to the instance variable. Its final implementation code is as follows:

public class Singleton {
    private Singleton() {}
    // Use volatile to prevent instruction reordering
    private static volatile Singleton instance = null; // [mainly because the code in this line has changed]
    public static Singleton getInstance() {
        if (instance == null) { // ①
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton(); // ②
                }
            }
        }
        return instance;
    }
}

summary

Volatile is an important part of Java Concurrent Programming. It has two main functions: ensuring the visibility of memory and prohibiting instruction reordering. Volatile is often used in one write and multiple read scenarios, such as the CopyOnWriteArrayList set. It will copy all the data during the operation and lock the write operation. After the modification, use the setArray method to assign the array to the updated value. Using volatile can make the reading thread quickly inform that the array has been modified without instruction rearrangement. After the operation is completed, it can be visible to other threads.

Right and wrong are judged by ourselves, bad reputation is heard by others, and the number of gains and losses is safe.

Official account: analysis of Java interview questions

Interview collection: https://gitee.com/mydb/interview

Tags: Java Interview

Posted by blackhawk08 on Thu, 05 May 2022 09:37:01 +0300