What does volatile do?

Volatile serves two main purposes:

  • Ensure memory visibility of variables
  • Disallow instruction reordering

What does memory visibility mean?

Multithreaded communication in Java mainly through:

  • By sharing memory data
  • Through the message notification mechanism

So how do JVMS communicate through shared memory? Thread A and thread B each copy from the main memory to their own local memory, modify the variable data, and then refresh to the main memory. As shown in figure:

Graph TB thread A((thread A)) thread B((thread B)) main memory (main memory) thread A memory (memory copy of thread A) thread B memory (memory copy of thread B) main memory --> thread A memory main memory --> thread B memory thread A memory --> thread A memory thread B memory Thread B thread A memory -- refresh --> main memory Thread B memory -- refresh --> main memory

By modifying variables to achieve implicit communication, but the reality is often unsatisfactory, as expected the following code A,B thread will print alternately:

package com.example.lib_java; public class MyTest { static boolean flag = true; public static void main(String[] args) throws InterruptedException { AThread a = new AThread(); BThread b = new BThread(); a.start(); Thread.sleep(500); // make sure that b starts b.start() after A; } static class AThread extends Thread{ @Override public void run() { for(;;) { if(MyTest.flag){ System.out.println("thread A print"); MyTest.flag = false; } } } } static class BThread extends Thread{ @Override public void run() { for(;;) { if(! MyTest.flag){ System.out.println("thread B print"); MyTest.flag = true; } } } } }Copy the code

Running the code, we find that there is no alternate printing because the child thread does not flush the data to main memory immediately after making changes, and other threads cannot read the latest changes. Static volatile Boolean flag = true; static volatile Boolean flag = true; We found that we can print alternately. What’s going on here?

When volatile is used to modify a shared variable, each thread will copy the variable from main memory to local memory as a copy. When a thread manipulates a variable copy and writes it back to main memory, the CPU bus sniffing mechanism informs other threads that the variable copy is invalid and needs to be read from main memory again. Volatile ensures the visibility of shared variables by different threads. That is, when a volatile variable is written back to main memory by one thread, the other threads immediately see its latest value. This is the memory visibility of volatile.

Disallow instruction reordering

We know that Java divides the code we write into instructions, which may be rearranged for efficiency. This property can have some unintended consequences in multithreading, such as in our common singleton pattern:

public class Singleton {
    private volatile static Singleton instance;
    private Singleton() {
    }

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
Copy the code

Here are two questions:

  • Why volatile when you already have synchronized blocks?
  • Given that volatile has memory visibility, why use synchronized for thread safety?

Instance = new Singleton(); The execution of this line of code can be decomposed into the third step :(1) allocate memory for instance. (2) Execute Singleton constructor to initialize instance. (3) Set instance to the allocated memory.

However, before JDK1.5, the sequence of (2)(3) cannot be guaranteed. If the sequence of (1)(3)(2) is followed by (1)(3)(2), if thread A finishes (3) and (2) is not executed, it will be switched to thread B, because step (3) has been executed in thread A, thread B will directly take the instance that is not empty. This invalidates the double-checked locking judgment.

After JDK1.5, just declare instance real: Private Volatile static Singleton instance; The addition of the volatile modifier ensures that the program is executed in accordance with (1), (2), and (3).

Therefore, volatile is mandatory.

The second question actually asks: Why does volatile not guarantee atomicity?

Why does volatile not guarantee atomicity?

The atomicity of the program means that all operations in the whole program are either completed or not completed, and cannot be stopped in some intermediate link. Atomicity is uninterruptible in an operation, and either all executions succeed or all executions fail, with a sense of “live or die together”. Even when multiple threads are working together, an operation can be started without interference from other threads.

Volatile does not guarantee thread synchronization, as the following code verifies:

package com.example.lib_java; public class MyTest { static volatile int count = 0; public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 5; i++) { Thread thread = new Thread(new MyRunnable()); thread.start(); } Thread.sleep(2000); System.out.println("count is "+ count); } static class MyRunnable implements Runnable{ @Override public void run() { for (int i = 0; i < 10_000; i++) { count++; }}}}Copy the code

Executing the code above, we started five threads, each adding 10,000 times. The expected output was 50,000, but the output was different each time, indicating that volatile did not synchronize threads (i.e., atomicity). Change the code to:

package com.example.lib_java; import java.util.concurrent.atomic.AtomicInteger; public class MyTest { static AtomicInteger count = new AtomicInteger(0); public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 5; i++) { Thread thread = new Thread(new MyRunnable()); thread.start(); } Thread.sleep(2000); System.out.println("count is "+ count.get()); } static class MyRunnable implements Runnable{ @Override public void run() { for (int i = 0; i < 10_000; i++) { count.incrementAndGet(); }}}}Copy the code

We found that we can print the expected value. This is because AtomicInteger has atomic properties. For details, see “CAS for Interview questions, Do you understand? Why isn’t volatile atomic?

  • In short, there are four steps to changing volatile variables:
  • 1) Read volatile variables to local
  • 2) Modify variable values
  • 3) Write back the local value
  • 4) Insert a memory barrier, the LOCK instruction, to make it visible to other threads
  • This makes it easy to see that the first three steps are not safe, and there is no guarantee that there will be no other thread modification between the value and the write back. Atomicity is guaranteed by locking.

Okay, so much for volatile.