I interviewed with several companies last year, and almost every company asked about volatile, even in every interview round. Volatile is a popular question for interviewers because it can be used to refer to Java memory models, memory barriers, happen-befor, etc., which can be used to explore system instructions, hyper-threading, etc.

Java Memory Model (JMM)

Volatile is the lightest synchronization mechanism provided by the Java VIRTUAL machine, but it is difficult to understand and use properly. It may be helpful to understand volatile by studying the special access rules that the Java memory model defines for volatile.

The Java memory model defines the relationship between threads and memory: shared variables between threads are stored in main memory, and each thread has a private local memory that stores copies of shared variables to read/write. Local memory is an abstraction of the JMM and does not really exist; It covers memory, caches, registers, and other hardware and compiler optimizations. Java’s memory model is abstracted as follows:

Semantics of volatile

Volatile provides two main semantics:

1. Visibility:

Visibility is a value written by one thread that can be read immediately by other threads. As known from the Java memory model, each thread has local memory. So thread A writes something that under normal circumstances thread B cannot read immediately. However, with volatile variables, thread A is guaranteed to write directly to main memory without writing to local memory, and thread B is guaranteed to read directly from main memory without reading from local memory.

2, forbid instruction reordering:

Reordering is an optimization method by which compilers and processors reorder instructions to optimize program performance.

Several reorders of Java programs

  1. Compiler optimization reordering: The compiler can rearrange the execution order of statements without changing the semantics of a single-threaded program.

  2. Instruction-level parallel reordering: If there is no data dependency, the processor can change the execution order of the machine instructions corresponding to the statement.

  3. Reordering of memory systems: Processors use caches and read/write buffers, which make loading and storing operations appear to be out of order

The technical cornerstone of Volatile is memory barriers

Memory barriers are CPU instructions that guarantee the ordering of certain operations and the visibility of certain memory. Inserting a barrier instruction tells the compiler and CPU that no instruction can be reordered with the barrier instruction. Another thing that memory barriers do is force various CPU caches to flush. A write-barrier, for example, will flush out all data written to the cache before the Barrier, so that any thread on the CPU can read the latest version of the data.

Java programs that generate assembly code for both volatile and non-volatile code will find that volatile code has an extra lock prefix.

Typical use cases for volatile

A code example for the status flag is as follows:

While thread 1 is executing run(), another thread 2 May have called shutdown, so the stop variable must be volatile (to take advantage of the visibility of volatile).

There is also a common use in the implementation of double-checked singletons with the following code:

Instance = new Singleton(), which is not an atomic operation, actually does three things in the JVM:

  1. Allocate memory to instance

  2. Call the constructor of the Singleton to initialize a member variable

  3. Reference instance to the allocated memory space (instance is not null after this step)

If the instance variable is not volatile, because of the instruction reordering, the procedure may be 1-2-3 or 1-3-2. Once it is 1-3-2, it may cause access to uninitialized memory. But with the volatile keyword, be sure to do it 1-2-3 (using volatile’s prohibition against reordering).