preface

The volatile keyword plays a critical role in Java concurrent programming, and is often a must-have question in interviews. This article describes the use of the volatile keyword and how it is implemented.

Volatile role

Volatile plays an important role in concurrent programming. Volatile is lightweight synchronized. The keyword volatile serves two purposes:

1) Ensure visibility of shared variables

Visibility means that when one thread modifies a shared variable, another thread can read the changed value. Local memory holds a copy of a shared variable. Threads make changes to shared variables by first modifying the copy of local memory and then writing it back to main memory.

Possible situation, thread and thread B to modify A Shared variable C at the same time, assuming that thread A first to Shared variables have modified C, while the thread B failed to timely to Shared variables C perception has changed, and then A copy of the B for local overdue data is modified, which caused the Shared variables are not visible.

Shared variables that are volatile are flushed to main memory and invalidated by other threads, ensuring visibility between threads.

2) Prevent instruction reordering

Another use of the volatile keyword is to prevent instruction reordering. In the actual execution process, the code is not always executed in the order written. Under the condition that the single-thread execution result is guaranteed, the compiler or CPU may reorder the instructions to improve the execution efficiency of the program. In multithreaded situations, however, instruction reordering can cause some problems, the most common being the double checklock singleton:

public class SingletonSafe {

    private static volatile SingletonSafe singleton;

    private SingletonSafe(a) {}public static SingletonSafe getSingleton(a) {
        if (singleton == null) {
            synchronized (SingletonSafe.class) {
                if (singleton == null) {
                    singleton = newSingletonSafe(); }}}returnsingleton; }}Copy the code

If the volatile keyword is not used, another thread may acquire an uninitialized Singleton object for reasons I won’t cover here. If you are interested, you can search “double checked locking with delay initialization” for further study. The author will write an article for further analysis later.

How volatile works

1) The implementation principle of visibility

For volatile variables, the JVM sends a lock prefixed instruction to the processor to write the cached variable back into system main memory when volatile variables are written. However, even if it is written back to memory, if the value cached by other processors is still old, it will be a problem to perform the calculation operation. Therefore, in multi-processors, to ensure that the cache of each processor is consistent, the cache consistency protocol is implemented.

Cache consistency protocol: Each processor by sniffing the spread of the data on the bus to check the value of the cache is expired, when the processor found himself cache line corresponding to the memory address has been changed, and will be set for the current processor cache line in invalid state, when the processor wants to modify the data operation, will be forced to read the data from the system memory to the processor cache.

So, if a variable is volatile, its value is forcibly flushed into main memory after each data change. The caches of other processors also load the value of this variable from main memory into their caches because they comply with the cache consistency protocol. This ensures that the value of a volatile is visible in multiple caches in concurrent programming.

2) Implementation principle of instruction reordering prevention

Volatile prevents instruction reordering through memory barriers. There are three types of memory barriers:

Store Barrier

The Store barrier, which is the x86 “sfence” directive, forces all Store directives that come before the Store barrier directive to be executed before the Store barrier directive is executed.

Load Barrier

The Load barrier, which is the “ifence” instruction on x86, forces all loads after the Load barrier instruction to be executed after the Load barrier instruction is executed

Full Barrier

The Full barrier, which is the “mfence” instruction on x86, combines the functionality of the load and save barriers.

In the Java memory model, volatile variables insert a store barrier after writes and a Load barrier before reads. A final field of a class is inserted with a store barrier after initialization to ensure that the final field is visible when the constructor is initialized and ready for use. It is also the JMM that inserts memory barriers before and after volatile variables are read and written to ensure that they are executed sequentially.