When a variable is declared volatile, the read/write of that variable is special. Volatile is lightweight synchronized. If a variable is volatile, it is less expensive than synchronized because it does not cause thread context switching and scheduling.

Java memory model

These three basic concepts are commonly encountered in concurrent programming: atomicity, visibility, and orderliness.

atomic

Atomicity means that an operation or operations are either all performed without interruption by any factor, or none at all. Like transactions in a database, they live and die together as a team. For example,

example Atomicity or not explain
i=1 is In Java, variables and assignments to primitive data types are atomic operations
j=i no There are two operations: read I and assign the value of I to j
i++ no There are three operations: read I, I +1, and assign the +1 result to I
j=i+1 no There are three operations: read the value of I, I +1, and assign the +1 result to j

In a single-threaded environment we can think of the entire step as atomic, but in a multi-threaded environment Java only guarantees atomic variables and assignments of primitive data types. To ensure atomicity in a multithreaded environment, use locking, synchronized (volatile does not guarantee atomicity for compound operations).

visibility

Visibility means that when multiple threads access the same variable and one thread changes the value of the variable, other threads can immediately see the changed value.

In a multithreaded environment, one thread’s operation on a shared variable is invisible to other threads. Java provides volatile to ensure visibility. When a variable is volatile, thread-local memory is invalid. When a thread changes a shared variable, it is immediately updated to main memory, and when other threads read the shared variable, it is read directly from main memory. Both Synchronize and locks ensure visibility.

order

Orderliness means that the order in which a program is executed is the order in which the code is executed.

In the Java memory model, the compiler and processor are allowed to reorder instructions for efficiency. Reordering does not affect the results of a single thread, but does affect multiple threads. Java provides volatile to ensure some order. The most famous example is the DCL (double-checked lock) in the singleton pattern.

Principle of volatile

Volatile guarantees thread visibility and provides some order, but not atomicity. Volatile is implemented using “memory barriers” underneath the JVM.

Thus, the characteristics of volatile can be summarized as follows

  1. Guaranteed visibility, not guaranteed atomicity (guaranteed atomicity only for read/write of individual volatile variables)
  2. Disallow instruction reordering
  3. The underlying volatile is implemented using “memory barriers.

Volatile and happens-before

Happens-before is the main basis for determining whether data is stored competitively and whether threads are safe. It ensures visibility in a multi-threaded environment.

Happens-before Rules about volatile

Volatile variable rule: Writes to a volatile field, happens-before any subsequent reads to that volatile field. When a volatile variable is written, the JMM flusher the value of the shared variable from the thread’s local memory to main memory. When a volatile variable is read, the JMM invalidates the thread’s local cache and reads the variable from main memory.

From the perspective of memory semantics, volatile write-read has the same memory effect as lock release-acquire. Volatile writes and lock release-acquire have the same memory semantics. Volatile reads have the same memory semantics as lock acquisition.

Sample code:

    int a = 0;
    volatile boolean flag = false;

    //Thread A
    public void writer(){
        a = 1;              //1
        flag = true;        //2
    }

    //Thread B
    public void reader() {if(flag){           //3
            int i = a;      //4
        }
    }
Copy the code

According to the happens-before principle, the following relation can be obtained for the above procedure:

  1. According to the happens-before sequence principle: 1 happens-before 2, 3 happens-before 4;
  2. According to the happens-before principle for volatile: 2 happens-before 3;
  3. 1) Before 4) before 4) before

Operations 1 and 4 have a happens-before relationship, so 1 must be visible to 4. At this point, 1 and 2 will not be reordered because the current flag is modified by volatile, which will prohibit reordering. So all shared variables visible to thread A before writing to A volatile will immediately become visible to thread B after thread B reads the same volatile variable.

Volatile memory semantics

In THE JMM, communication between threads is implemented using shared memory. When a volatile variable is written, the JMM immediately flusher the value of the shared variable from the thread’s local memory to main memory. When a volatile variable is read, the JMM invalidates the thread’s local memory and reads the shared variable directly from main memory.

So the write memory semantics of volatile are flushed directly into main memory, and the read memory semantics are read directly from main memory. General variables are reordered, but volatile variables are not. This affects their memory semantics, so the JMM limits reordering in order to implement the memory semantics of volatile.

Volatile write-read memory semantics

When a volatile variable is written, the JMM immediately flusher the value of the shared variable from the thread’s local memory to main memory. When a volatile variable is read, the JMM invalidates the thread’s local memory and reads the shared variable directly from main memory

Volatile write memory semantics

In the example above, thread A executes writer() first and thread B executes reader(). At the beginning, flag and A in the local memory of both threads are in the initial state. The following diagram shows the state of the shared variable after volatile write by thread A.

After thread A writes flag variables, the values of the two variables updated by thread A in local memory A are flushed to the main memory. In this case, the values of shared variables in local memory A and main memory are the same.

Volatile reads memory semantics

Proceed with thread B’s Reader () method, and the following is a diagram of shared variables that thread B volatile reads.

As shown in the figure, after reading the flag variable, the value contained in local memory B has been set to invalid. At this point, thread B must read the shared variable from main memory. The read operation of thread B causes the values of the shared variables in local memory B and main memory to be consistent.

If the steps of volatile write and volatile read are combined, after thread B reads A volatile variable, all the values of the shared variables visible to thread A before writing to the volatile variable become immediately visible to thread B

Volatile read-write memory semantics summary
  • Writing A volatile variable on thread A essentially means that thread A expects the next thread to read the volatile variable to send A message.
  • Thread B reads a volatile variable, essentially accepting a message from a previous thread that made changes to the shared variable before writing to the volatile variable.
  • Thread A writes A volatile variable, and thread B reads the volatile variable. Essentially, thread A sends A message through main memory to thread B.

Volatile reordering

Volatile reorder table:

Is it possible to resort Second operation Second operation Second operation
The first operation Ordinary reading/writing Volatile read Volatile write
Ordinary reading/writing no
Volatile read no no no
Volatile write no no
  1. If the first operation is a volatile read, no matter what the second operation is, it cannot be reordered. This operation ensures that operations after volatile reads are not reordered by the compiler to those before volatile reads.
  2. When the second operation is volatile write, reordering is not possible regardless of the first operation. This operation ensures that operations prior to volatile writes will not be reordered by the compiler after volatile writes.
  3. If the first operation is volatile write and the second operation is volatile read, reorder cannot be performed.

Volatile underlying implementation

The underlying implementation of volatile is by inserting a memory barrier.

  • Insert a StoreStore barrier before each volatile write
  • Insert a StoreLoad barrier after each volatile write
  • Insert a LoadLoad barrier after each volatile read
  • Insert a LoadStore barrier after each volatile read

The above memory barrier insertion strategy is conservative, but it ensures that volatile memory semantics are correct for any pair of programs on any processor platform.

Volatile write memory barriers

Volatile write insert memory barrier instruction sequence diagram:

The StoreStore barrier ensures that all prior common writes are visible to any processor before volatile writes, because the StoreStore barrier guarantees that all common writes are flushed to main memory before volatile writes. The purpose of StoreLoad is to avoid reordering volatile writes from potentially volatile reads/writes. This is because the compiler is often unable to accurately determine whether a StoreLoad barrier needs to be inserted after a volatile write, such as a volarile write after which the method returns immediately. To ensure that the memory semantics of volatile are properly implemented, the JMM takes a conservative approach of inserting a StoreLoad barrier after or in front of each volatile write. Choosing to insert the StoreLoad barrier after volatile writes provides considerable performance when readers greatly outnumber writers.

Volatile reads memory barriers

Volatile read insert memory barrier instruction sequence diagram:

The LoadLoad barrier prevents processors from reordering volatile reads from ordinary writes

The above memory barrier insertion strategy for volatile writes and volatile reads is very conservative. During actual execution, the compiler can omit unnecessary barriers as long as the memory semantics of volatile write-read are not changed.

The underlying implementation of Volatile

This is what I learned from listening to the open class online.

Volatile, in some cases, is more convenient than locking. If a field is declared volatile, the Java thread memory model ensures that all threads see the same value of the variable.

See the most realistic running details of Java code by looking at Java’s assembly instructions.

Writes to volatile variables are preceded by a lock quality prefix. The lock prefix causes the CPU’s Cache to write to memory, and the write also causes other cpus to invalidate their Cache. This null operation makes the previous changes to the volatile variable immediately visible to other cpus. So its functions can be considered as following three

  1. Lock the main memory
  2. Any read must be performed after the write is complete
  3. Invalidate the stack cache of this value for other threads

The JMM and CPU analyze the principle of volatile

Continue analyzing this string of code

    int a = 0;
    volatile boolean flag = false;

    //Thread A
    public void writer(){
        a = 1;              //1
        flag = true;        //2
    }

    //Thread B
    public void reader() {if(flag){           //3
            int i = a;      //4
        }
    }
Copy the code

The JMM and CPU analysis figure is shown below

  1. Read Old value, obtained through the bus
  2. Load old value, load old value into the working memory
  3. Use The old value. The CPU uses the old value in the working memory
  4. The CPU assigns the new value to the working memory
  5. Store new value, working memory will be the new value store, and it will send the Lock prefix instruction to the processor
  6. Write new value, write new value to main memory, write post unlock. (Note: The new and old values are relative here)

On volatile writes, the JVM sends the processor an instruction with a Lock prefix that causes the processor cache to be written to memory. The Lock signal ensures that the processing can monopolize any shared memory while the signal is spoken. But the Lock signal generally does not Lock the bus, but locks the cache, because the cost of locking the bus is higher than the cost of the bus. Then, the cache written back to memory by one processor invalidates the caches of other processors, according to the MESI control protocol to maintain consistency between the internal cache and the caches of other processors. That is, according to the code above, thread B’s working memory cache is flushed, and the memory is read again. This ensures visibility of volatile.

conclusion

Volatile ensures order and visibility.

  • Volatile uses “memory barriers” to prevent instructions from being reordered, thereby ensuring that code execution is ordered.
  • Valatile writes the processor’s cache back to memory using the Lock# prefix, ensuring visibility of variables according to the MESI control protocol.

Without changing the memory semantics of volatile write-read, the compiler can omit unnecessary barriers on a case-by-case basis.

The Art of Concurrent Programming in Java

My humble opinion, thank you for reading. Welcome to the discussion,Personal blog

JAVA concurrency (1) Concurrency programming challenges

JAVA concurrency (2) Underlying implementation principles of synchronized and volatile

JAVA concurrency (3) lock status

JAVA concurrency (4) atomic operation implementation principle

JAVA concurrency (5) happens-before

JAVA concurrency (6) reordering

JAVA concurrency (7) Analyze volatile against the JMM

JAVA concurrency (8) Final domain

JAVA concurrency (9) In-depth analysis of DCL

JAVA Concurrency (10) Concurrency programming basics