Java concurrent programming-volatile

volatile is a lightweight synchronization mechanism provided by the java virtual machine. It has three important features:

  • Ensure visibility
  • No guarantee of atomicity
  • Disable command rearrangement

To understand these three features, you need to have a certain understanding of JMM (JAVA memory model).

Main problems solved:
In the JVM, each thread will have local memory. The local memory is a copy of the public memory. The local memory of each thread is isolated from each other. There will be a situation where one thread modifies the shared variable and other threads are not aware of it, resulting in data corruption. inconsistent

1. JMM (JAVA memory model)

JMM is a memory model defined in the Java virtual machine specification. The Java memory model is standardized, shielding the differences between different underlying computers. the difference. In other words, JMM is an underlying model mechanism for concurrent programming defined in JVM. JMM defines the abstract relationship between threads and main memory (which can be understood as 8/16G memory when buying a computer): shared variables between different threads exist in the main memory, and each thread exists A private local memory. Operations on shared variables require a copy of the shared variables in the main memory to the local memory. That is, a copy of the shared variable exists in each thread’s local memory.

JMM provisions on synchronization:

  • 1. Before the thread is unlocked, the value of the shared variable must be refreshed to the main memory.
  • 2. Before the thread locks, it must read the latest shared variable value in the main memory to the local memory.
  • 3. Adding and unlocking are the same lock

When each thread is created, JVM will allocate working memory (also called stack space) to it. Working memory is the private area of each thread. The java memory model stipulates that all variables must be stored in the main memory. The main memory is a shared area and can be accessed by all threads. However, the thread’s operation on the variable must be performed in the working memory. The general process is that the thread copies the value of the variable from the main memory to the local memory, performs the operation, and then writes it back to the main memory. Since the working memory between different threads is not visible to each other, communication in all threads must be carried out through main memory. The specific process is as follows:

java memory model

Due to the mechanism of JMM, it leads to the problem of visibility.

Three major features of JMM

  • visibility
  • atomicity
  • Orderliness

2. Visibility

Memory visibility means that when a thread modifies the value of a variable, other threads can always know the change in this value.

Here is an example to illustrate:

package com.fzkj.juc;

import java.util.concurrent.TimeUnit;

/**
 * @DESCRIPTION volatile keyword test class
 */
public class VolatileTest {<!-- -->

    public static void main(String[] args) {<!-- -->
        Number number = new Number();

        new Thread(() -> {<!-- -->
            try {<!-- -->
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {<!-- -->
                e.printStackTrace();
            }
            number.numTo(20);
            System.out.println(Thread.currentThread().getName() + ":\tThe value of number is: " + number.num);
        }, "A thread").start();

        while(number.num == 0){<!-- -->}

        System.out.println(Thread.currentThread().getName() + ":\tThe value of number is: " + number.num);
    }


}

class Number{<!-- -->
    int num = 0;

    public void numTo(int target){<!-- -->
        this.num = target;
    }

    public void add(){<!-- -->
        this.num + + ;
    }
}

If you run the above example, you will find that the program will fall into an infinite loop and never output the last sentence. It is because the modification of the variable num in thread A is not visible to the main thread, causing the while loop to continue.

Common solutions to visibility issues include:

  • Lock
  • volatile keyword

volatile

Modify the above code

class Number{<!-- -->
    volatile int num = 0;

    public void numTo(int target){<!-- -->
        this.num = target;
    }

    public void add(){<!-- -->
        this.num + + ;
    }
}

This is how you run the above example. You won’t be stuck in an infinite loop anymore.

How does volatile ensure visibility?

How do other threads know that shared variables have been modified?
In order to solve the cache consistency problem, some protocols need to be followed, called cache consistency protocols, such as: MSI, MESI (IllinoisProtocol), MOSI, Synapse, Firefly and DragonProtocol, etc.

Sniffing

Use the sniffing mechanism to ensure that you know in time that your cache has expired.
Under multi-processors, in order to ensure that the caches of each processor are consistent, a cache coherence protocol will be implemented. Each processor checks whether its cached value has expired by sniffing the data spread on the bus.
When the processor finds that the memory address corresponding to its cache line has been modified, it will set the current processor’s cache line to an invalid state. When the processor modifies this data, it will read the data from the system memory again. processor cache

Since the sniffing mechanism will constantly monitor the bus, using volatile may cause bus storm

3. Atomicity

Let’s look at another situation.

package com.fzkj.juc;

import java.util.concurrent.TimeUnit;

/**
 * @DESCRIPTION volatile keyword test class
 */
public class VolatileTest {<!-- -->

    public static void main(String[] args) {<!-- -->
        atomicity();
    }

    // atomicity
    public static void atomicity(){<!-- -->
        Number num = new Number();
        for (int i = 0; i < 10; i + + ) {<!-- --> // Start 10 threads
            new Thread(() -> {<!-- -->
                for (int j = 0; j < 1000; j + + ) {<!-- --> // Each thread operates on the value of num 1000 times
                    num.add();
                }
            }).start();
        }
        try {<!-- -->
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {<!-- -->
            e.printStackTrace();
        }

        System.out.println(num.num);
    }
}

class Number{<!-- -->
     int num = 0;

    public void numTo(int target){<!-- -->
        this.num = target;
    }

    public void add(){<!-- -->
        this.num + + ;
    }
}

In the above example, we started 10 threads, each thread called the add method 1000 times, and accumulated the value of num by 1000. Then the final result we expect is that the value of num is 10000. But when you actually run the program, you will find that the results will be less than 10,000 every time.

The cause of this problem is actually related to JVM. We all know that the code written by the programmer is only for the programmer to see, and the code needs to be compiled before the machine can execute it. After a + + operation is compiled into a bytecode file, it can be simplified into three steps. The first step is to get the value; the second step is to add one; the third step is to assign the value. Therefore, in high concurrency scenarios, values may be overwritten.

The definition of atomicity: means that in a set of operations, either all operations succeed or all operations fail.

Atomicity is one of the features of JMM, but volatile does not support atomicity. To ensure atomicity in a multi-threaded environment, you can use the lock mechanism or the AtomicInteger.

4. Orderliness

Prohibiting the rearrangement of instructions is called orderliness.

What is instruction reordering?

In order to improve performance, compilers and processors often reorder instructions while complying with as-if-serial semantics. In the case of multi-threading, instruction rearrangement may lead to some unexpected situations.

So how does volatile prohibit the reordering of instructions? Here comes a new concept: Memory Barrier

Memory Barrier

The function of memory barriers is to prohibit instruction reordering and solve memory visibility problems.

First understand the two instructions:

  • store: refresh the data in the cache into memory
  • load: Copy the data stored in memory to the cache

JMM mainly divides memory barriers into four categories

Barrier type Instruction example Description
LoadLoad Load1;LoadLoad;Load2 Ensure that Load1 data is loaded before Load2
StoreStore Store1;StoreStore;Store2 Ensure that Store1 immediately refreshes data to memory before Store2
LoadStore Load1; LoadStore;Store2 Ensure that Load1 data is loaded before Store2 data is refreshed
StoreLoad Store1StoreLoad;Load2 Ensure that Store1 data is refreshed before Load2 data is loaded

StoreLoad is called an all-purpose barrier because it has the effects of the other three barriers, but it consumes more than other barriers.

Knowing this, let’s take a look at how volatile inserts a memory barrier.

volatile memory barrier

can be seen,

  • volatile adds LoadLoad and LoadStore barriers after the read operation
  • StoreStore and StoreLoad barriers are added before and after the write operation.

This means that the compiler will not reorder volatile reads and operations that follow a read; it will not reorder writes and operations that precede a write. This ensures the orderliness of volatile itself.