Volatile is an important part of concurrent programming and one of the most frequently asked interview questions. Don’t be like Jack Bauer, who missed the big factory because volatile is a lightweight synchronized.

Volatile has two major features: it guarantees visibility of memory and disallows reordering of instructions. So what is visibility and order reordering? Let’s see.

Memory visibility

To understand the memory visibility to from the Java memory model (JMM), all of the Shared variables in Java in main memory, each thread has its own working memory, in order to improve the running speed of the thread, each thread’s working memory will make a copy of Shared variables in main memory cache, in order to improve the operation efficiency, The memory layout is shown in the following figure:

However, this creates a new problem. If one thread changes the value of a shared variable, the other thread does not know that the value has been changed, and the two threads have inconsistent values. Let’s demonstrate this problem in code.

public class VolatileExample {
    // The visibility parameter
    private static boolean flag = false;

    public static void main(String[] args) {
		new Thread(() -> {
            try {
                // Pause the execution for 0.5s
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            flag = true;
            System.out.println("Flag changed to true");
        }).start();
        
        // Always loop to check flag=true
        while (true) {
            if (flag) {
                System.out.println("Flag detected to be true");
                break; }}}}Copy the code

The execution results of the above procedures are as follows:

Flag is changed to true

We will find that the result of flag changing to true will never be detected. This is because the non-main thread has changed flag=true, but the main thread never knows that this value has changed. This is a memory invisibility problem.

Memory visibility means that after a thread modifies the value of a variable, other threads immediately know that the value has changed.

We can use volatile to modify flag to ensure memory visibility, as follows:

public class VolatileExample {
    // The visibility parameter
    private static volatile boolean flag = false;

    public static void main(String[] args) {
		new Thread(() -> {
            try {
                // Pause the execution for 0.5s
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            flag = true;
            System.out.println("Flag changed to true");
        }).start();
        
        // Always loop to check flag=true
        while (true) {
            if (flag) {
                System.out.println("Flag detected to be true");
                break; }}}}Copy the code

The execution results of the above procedures are as follows:

Flag changes to True. Flag is changed to true

Instruction rearrangement

Instruction reorder means that when executing a program, compilers and processors often reorder instructions to improve the performance of the program. Such as jack going to the library book you borrowed last time also, to borrow a book casually, when my roommate wang also want to let jack Bauer is a book for him, not order rearrangement, jack Bauer first put their own things done, to do a roommate again, it obviously is a waste of time, there is one way of doing this is that he put his books and wang paid together, Get yourself a new book. That’s what reordering is all about.

However, instruction reordering does not guarantee the order in which instructions are executed, which creates a new problem, as shown in the following code:

public class VolatileExample {
    // Order to reorder parameters
    private static int a = 0, b = 0;
    private static int x = 0, y = 0;

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            Thread t1 = new Thread(() -> {
                // It is possible for a reorder of instructions to occur, with x=b and a=1
                a = 1;
                x = b;
            });
            Thread t2 = new Thread(() -> {
                // It is possible for a reorder of instructions to occur, first y=a and then b=1
                b = 1;
                y = a;
            });
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            System.out.println("The first" + i + "Time, x =" + x + " | y=" + y);
            if (x == 0 && y == 0) {
                // An instruction reorder occurred
                break;
            }
            // Initialize variables
            a = 0;
            b = 0;
            x = 0;
            y = 0; }}}Copy the code

The execution results of the above programs are as follows:

We have shown the reordering and memory visibility issues in code, and we will show the issues with volatile synchronization in code.

Volatile Indicates the asynchronous mode

If we use volatile integers to execute ++ and — the same number of times, the result is not zero. If we use volatile integers to execute ++ and — the same number of times, the result is not zero.

public class VolatileExample {
    public static volatile int count = 0; / / counter
    public static final int size = 100000; // Number of loop tests

    public static void main(String[] args) {
        / / + +
        Thread thread = new Thread(() -> {
            for (int i = 1; i <= size; i++) { count++; }}); thread.start();/ / - way
        for (int i = 1; i <= size; i++) {
            count--;
        }
        // Wait for all threads to finish executing
        while (thread.isAlive()) {}
        System.out.println(count); // Print the result}}Copy the code

The execution results of the above procedures are as follows:

1065

Synchronized = synchronized = synchronized = synchronized = synchronized = synchronized = synchronized

public class VolatileExample {
    public static int count = 0; / / counter
    public static final int size = 100000; // Number of loop tests

    public static void main(String[] args) {
        / / + +
        Thread thread = new Thread(() -> {
            for (int i = 1; i <= size; i++) {
                synchronized(VolatileExample.class) { count++; }}}); thread.start();/ / - way
        for (int i = 1; i <= size; i++) {
            synchronized(VolatileExample.class) { count--; }}// Wait for all threads to finish executing
        while (thread.isAlive()) {}
        System.out.println(count); // Print the result}}Copy the code

The result of this execution is the expected value of 0.

So volatile is not synchronized. So volatile is not synchronized. I know why the interviewer told me to go back and wait for the announcement.

Application Scenarios of Volatile

Since volatile only guarantees visibility for thread operations, what use is it? Volatile is bound to be problematic for read-over-write situations, but not for write-over-read situations. A classic example of volatile write-to-read use is CopyOnWriteArrayList. CopyOnWriteArrayList copies all data as it is written and locks the write. Using volatile allows the reader thread to quickly notify the array of changes, without reordering instructions. After that, the reader thread will be visible to other threads. The core source code is as follows:

public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess.Cloneable.java.io.Serializable {
    
    private transient volatile Object[] array;
    
	final void setArray(Object[] a) {
        array = a;
    }	
    / /... Ignore other code
}
Copy the code

conclusion

In this article, we demonstrate two features of volatile, memory visibility and the prohibition of reordering, in code, using ++ and — to demonstrate that volatile is not a lightweight form of synchronization. And the classic use case of volatile, CopyOnWriteArrayList.

Follow the qr code below and subscribe for more awesome content.