1. False cases

Introduce the volatile keyword with an example, such as the following code example: Without the volatile keyword, communication between the two threads would be problematic

public class ThreadsShare {
  private static boolean runFlag = false; // Volatile is not used
  public static void main(String[] args) throws InterruptedException {
    new Thread(() -> {
      System.out.println("Thread one waits to execute");
      while(! runFlag) { } System.out.println("Once the thread starts executing");
    }).start();
    Thread.sleep(1000);
    new Thread(() -> {
      System.out.println("Thread two begins execution.");
      runFlag = true;
      System.out.println("Thread two completes execution"); }).start(); }}Copy the code

Output result:

Conclusion: The thread did not sense that thread two had changed runFlag to true, so the statement “thread begins execution” was never printed and the program did not terminate

Like the following scene:

In the current scenario, it may occur that processor A and processor B did not flush the data in their respective write buffers back to the memory, and assigned the values of A = 0 and B = 0 read from the memory to X and Y. At this time, the data in the buffer was flushed into the memory, causing the final result to be inconsistent with the actual desired result. Because the buffer is not executed until the data is flushed into memory

Causes of this problem:

When a computer executes a program, every instruction is executed in the processor. In the execution of instructions, it is bound to involve data reading and writing. Program is running in the process of temporary data is stored in main memory of (physical memory), then there is a problem, due to the fast processor speed of execution, and from the memory read and write data to the memory process compared to the speed of the processor executes instructions are much more slowly, so if any time to the operation of the data will be made by interaction and memory, Can greatly slow down the execution of instructions. To solve this problem, a CPU cache is designed. When each thread executes a statement, it first reads the value from main memory, then copies it to local memory, and then performs data manipulation to flush the latest value to main memory. This creates a phenomenon of cache inconsistency

A cache consistency protocol, MESI, is proposed to solve the above problems

The idea is that the MESI protocol ensures that copies of shared variables used in each cache are consistent. When writing data processor, if found operating variables are Shared variables, which also has a copy of the variable in the other processors, sends a signal to notify the other processors to the Shared variables cache line set to invalid state sniffer mechanism (bus), so when other processor needs to read this variable, found himself in the cache cache the variable cache line is invalid, Then it will re-read from memory.

Sniffing cache consistency protocol:

All memory transfers take place on a shared memory bus, which is visible to all processors. The cache itself is independent, but the memory is shared. All memory access is arbitrated, meaning that only one processor can read or write data in the same instruction cycle. Processors not only interact with the memory bus during memory transfers, but also constantly swap data on the sniffing bus to track what other caches are doing, so when one processor reads or writes to memory, the other processors are notified (actively notified), which they use to synchronize their cache saves. As soon as one processor writes to memory, other processors know that the block in their cache is no longer valid.

The msci a:

In the MESI protocol each cache row has four states:

  1. Modified: indicates that the data is valid. The data is Modified and inconsistent with the data in memory. The data is stored only in the current cache
  2. Exclusive this row of data is valid, consistent with data in memory, and exists only in the local cache
  3. This line of data is valid, the data is identical to the data in memory, the data is stored in many caches,
  4. Invalid This row of data is Invalid

The values “Invalid”, “shared”, and “modified” comply with the sniffing cache consistency protocol, but “Exclusive” means Exclusive, valid, and consistent with data in memory. However, only in the current cache “Exclusive” state solves the problem that a processor must notify other processors before reading or writing to memory. The processor can write only when the cache row is “Exclusive” or “modified”, meaning that only in these two states does the processor own the cache row.

When a processor wants to write a cache line, it must first send a request for control to the bus if it has no control. This will tell other processors to invalidate their copies of the same cache segment. As long as in the gain control of the processor can modify the data, and the processor at this time until the cache line is only a copy and is only in its cache, there won’t be any conflict, on the other hand if other processor has been wanting to read the cache line, exclusive or modified cache line must first return to share state, if it is already modified cache line, You have to write it back into memory first

So Java provides a lightweight synchronization mechanism, volatile

2. The role

Volatile is a lightweight synchronization mechanism provided by Java. Volatile is lightweight because it does not cause thread context switching and scheduling. But volatile variables are less synchronized, do not guarantee the synchronization of a block of code, and are more error-prone to use. The volatile keyword is used to ensure visibility, that is, memory visibility of shared variables to address cache consistency issues. Once a shared variable is decorated with the volatile keyword, there are two levels of semantics: memory visibility and instruction reordering prohibition. In a multithreaded environment, the volatile keyword is used primarily to sense changes to a shared variable and to make the value of the variable immediately available to other threads

The effect of the volatile keyword:

Usage:

private volatile static boolean runFlag = false;
Copy the code

Code:

public class ThreadsShare {
  private volatile static boolean runFlag = false;
  public static void main(String[] args) throws InterruptedException {
    new Thread(() -> {
      System.out.println("Thread one waits to execute");
      while(! runFlag) { } System.out.println("Once the thread starts executing");
    }).start();
    Thread.sleep(1000);
    new Thread(() -> {
      System.out.println("Thread two begins execution.");
      runFlag = true;
      System.out.println("Thread two completes execution"); }).start(); }}Copy the code

Output result:

Conclusion: Thread one senses that thread two has changed runFlag to true, so the sentence “thread begins execution” is printed and the program terminates.

Volatile has two effects:

  1. When a thread writes a volatile variable, the JMM forces the value of the variable in the thread’s local memory to be flushed to main memory
  2. This write operation invalidates the cache of the shared variable in other threads and must be re-evaluated in main memory to use the variable.

Consider: What if two processors read or modify the same shared variable simultaneously?

To access memory, multiple processors must first acquire a memory bus lock. Only one processor can gain control of the memory bus at any time, so this does not happen.

Important: The volatile keyword is used to ensure visibility, that is, memory visibility of shared variables to address cache consistency issues


Characteristics of 3.

3.1 the visibility

When a shared variable is volatile, it guarantees that the value is immediately updated to main memory, and that it will read the new value in memory when another thread needs to read it. A common shared variable does not guarantee visibility, because it is not certain when a common shared variable is written to main memory. When a common shared variable is read by another thread, the old value may still be in memory, so visibility cannot be guaranteed.

3.2 Forbid instruction rearrangement

In the Java memory model, the compiler and processor are allowed to reorder instructions, but the reordering process does not affect the execution of a single-threaded program, but affects the correctness of multithreaded concurrent execution

The volatile keyword disallows instruction reordering in two ways:

  1. When a program performs a read or write to a volatile variable, all changes to the preceding operation must have occurred and the result must be visible to subsequent operations. The operation behind it has certainly not taken place;
  2. During instruction optimization, statements that access volatile variables cannot be executed after them or statements that access volatile variables cannot be executed before them.

To address memory errors caused by processor reordering, the Java compiler inserts memory barrier instructions at the appropriate locations in the generated instruction sequence to prohibit a particular type of processor reordering

Memory barrier instructions: Memory barriers are an implementation of the semantics of volatile, as explained below

Barrier type Order sample instructions
LoadLoadBarriers Load1; LoadLoad; Load2 Load1 data loading occurs before Load2 and all subsequent data loading
StoreStoreBarriers Store1; StoreStore; Store2 Data flushing from Store1 to main storage occurs before data flushing from Store2 and all subsequent data flushing from main storage
LoadStoreBarriers Load1; LoadStore; Store2 Load1 data loading occurs before Store2 and all subsequent data is flushed back to main memory
StoreLoadBarriers Store1; StoreLoad; Load2 Store1 data is flushed back to memory before Load2 and all subsequent data loads

4. The volatile and happens-before

public class Example {
  int r = 0;
  doublePI =3.14;
  volatile boolean flag = false; / / volatile
  /** * Data initialization */
  void dataInit(a) {
    r = 1; / / 1
    flag = true; / / 2
  }
  /** ** data calculation */
  void compute(a) {
    if(flag){ / / 3
      System.out.println(π * r * r); / / 4}}}Copy the code

If thread A executes dataInit(), thread B executes compute() according to the happens-before rule. The Java memory model says that step 2 must be volatile before step 3. Step 1 precedes Step 2 and Step 3 precedes Step 4, so step 1 also precedes Step 4 according to the transitivity rule.


5. Memory semantics

5.1 Read Memory Semantics

Reading a volatile variable invalidates the local working memory, fetching the current value of the volatile variable.

5.2 Write Memory Semantics

Writing a volatile variable forces the value of the local working memory to be flushed back into memory.

5.3 Implementation of memory semantics

The JMM list of volatile reordering rules for compilers

Can I reorder it Second operation
The first operation Ordinary reading or writing Volatile read Volatile write
Common or write NO
Volatile read NO NO NO
Volatile write NO NO

For example, the last cell in the third line means:

If a local operation is common and the second operation is volatile, the compiler cannot reorder the two operations

5.4 summarize

  1. When the second operation is a volatile write, nothing in the first operation can be reordered. This rule ensures that operations that precede volatile writes cannot be re-ranked by the compiler after volatile writes
  2. When the first operation is a volatile read, no matter what the second operation is, it cannot be reordered. This rule ensures that operations that follow volatile reads are not compiled by the compiler before volatile
  3. The first volatile write and the second volatile read cannot be reordered

To implement the memory semantics of volatile, the compiler inserts a memory barrier into the instruction sequence when it generates the bytecode to prevent a particular type of processor ordering.

JMM Memory barrier insertion strategy:

  1. Insert a StoreStore barrier before each volatile write.
  2. Insert a StoreLoad barrier after each volatile write.
  3. Insert a LoadLoad barrier after each volatile read.
  4. Insert a LoadStore barrier after each volatile read.

Volatile write the sequence of instructions generated after inserting the memory barrier:

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 StoreLoad barrier ensures that volatile writes are reordered from any volatile reads or writes that may follow.

Schematic diagram of the instruction sequence generated after volatile reads are inserted into the memory barrier:

The LoadLoad barrier prevents the processor from reordering volatile reads above from normal reads below.

The LoadStore barrier prevents the processor from reordering volatile reads above from normal reads below.


6. The actual combat

6.1 Conditions must be met for using Volatile

  • Writes to variables do not depend on the current value
  • This variable is not contained in an invariant with other variables

In effect, these conditions indicate that the valid values that can be written to volatile variables are independent of any program state, including the current state of the variables. In fact, the two conditions above are to ensure that the operation on the volatile variable is atomic, so that programs using the volatile keyword can execute correctly on concurrency

6.2 Main Usage Scenarios of Volatile

In a multi-threaded environment, the modification of shared variables is immediately sensed and the latest value of variables is immediately available to other threads

Scenario 1: State markers (examples in this article)

public class ThreadsShare {
  private volatile static boolean runFlag = false; // Status flag
  public static void main(String[] args) throws InterruptedException {
    new Thread(() -> {
      System.out.println("Thread one waits to execute");
      while(! runFlag) { } System.out.println("Once the thread starts executing");
    }).start();
    Thread.sleep(1000);
    new Thread(() -> {
      System.out.println("Thread two begins execution.");
      runFlag = true;
      System.out.println("Thread two completes execution"); }).start(); }}Copy the code

Scene two Double – Check

DCL singleton is short for Double Check Lock. The so-called double – end search is used to make a judgment before and after locking

public class Singleton1 {
    private static Singleton1 singleton1 = null;
    private Singleton1 (a){
        System.out.println("Constructor executed.....");
    }
    public static Singleton1 getInstance(a){
        if (singleton1 == null) {// First check
            synchronized (Singleton1.class){
                if (singleton1 == null) // The second check
                    singleton1 = newSingleton1(); }}returnsingleton1 ; }}Copy the code

Synchronized locks only the part of the code that creates the instance, not the whole method. Both before and after locking are judged, which is called the double-ended retrieval mechanism. This really only creates one object. But it is not entirely safe. New An object also has three steps:

  • 1. Allocate object memory space
  • 2. Initialize the object
  • 3. Point the object to the allocated memory address. The object is not null

Steps two and three have no data dependencies, so the compiler optimizations allow them to be reversed. When instructions are rearranged, multithreaded access can also cause problems. The result is the following final singleton pattern. This situation does not occur in order reordering

public class Singleton2 {
  private static volatile Singleton2 singleton2 = null;
  private Singleton2(a) {
    System.out.println("Constructor executed......");
  }
  public static Singleton2 getInstance(a) {
    if (singleton2 == null) { // First check
      synchronized (Singleton2.class) {
        if (singleton2 == null) // The second check
          singleton2 = newSingleton2(); }}returnsingleton2; }}Copy the code