This is the 15th day of my participation in the August More text Challenge. For details, see: August More Text Challenge

Phase to recommend

  • Java Basics
  • Java Concurrent programming

Introduction to Volatile

Volatile is a lightweight synchronization mechanism provided by Java. The Java language contains two internal synchronization mechanisms: synchronized blocks (or methods) and volatile variables, which are lighter than synchronized (often referred to as “heavyweight locking”) because they do not cause thread context switches and scheduling. However, volatile variables are less synchronized (sometimes they are simpler and less expensive), and their use is more error prone.

Volatile variables ensure that each thread has access to the latest value of the variable, thereby avoiding dirty reads.

Ii. Volatile action

2.1 Ensure visibility of shared variables

Visibility: When one thread modifies a shared variable, the other thread can read the change.

When a thread modifies a shared variable using the volatile keyword, it is immediately flushed to main memory and invalidates data cached by other threads. This ensures that the shared variable is visible between threads.

public class VolatileDemo1 {
    // Add volatile keyword when the main thread changes the shared variable num
    // Thread A, which holds the shared variable num, is notified to ensure that the shared variable is visible
    private volatile static int num = 0;
    public static void main(String[] args) throws InterruptedException {
        // The main thread changed the value of the shared variable num
        // If the keyword volatile is not used, thread A will not know that the value of the shared variable has changed and will enter an infinite loop
        new Thread(() -> {
            while (num == 0) {
    			//}},"A").start();
        TimeUnit.SECONDS.sleep(2);
        // The main thread modifs the value of the shared variable num and writes it back to main memory
        num = 1; System.out.println(num); }}Copy the code

2.2 Command reordering is prohibited

Reorder: Means by which compilers and processors order sequences of instructions to optimize program performance.

Java provides volatile to ensure a certain degree of order. Reordering operations do not reorder operations that have data dependencies to optimize performance, but no matter how the reordering is done, the results of a single-threaded program cannot be changed.

Singleton implementation: Double-checked Locking (DCL)

public class Singleton {
    public static volatile Singleton singleton;
    The /** * constructor is private, which disallows external instantiation */
    private Singleton() {};
    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (singleton.class) {
                if (singleton == null) {
                    singleton = newSingleton(); }}}returnsingleton; }}Copy the code

Since the operating system can reorder instructions, the object construction process, instantiating an object, might be as follows:

  • Allocate memory space.
  • Assigns the address of the memory space to the corresponding reference.
  • Initialize an object

If this is the process, a multi-threaded environment can expose an uninitialized object reference, leading to unexpected results. Therefore, to prevent reordering of the process, we need to set the variables to volatile.

2.3 Atomicity: Single read/write

Atomicity: One or more operations are either all performed and the process is not interrupted by any factor, or none of them are performed.

Volatile does not guarantee complete atomicity, only atomicity for a single read/write operation.

2.3.1 why can’t i++ guarantee atomicity?

  • rightvolatileA single read/write operation on a variable is guaranteed to be atomic;
  • rightlonganddoubleType variable, but there is no guarantee that i++ is atomic, because i++ is essentially a read and write operation.
public class VolatileTest01 {
    volatile int i;

    public void addI(){
        i++;
    }

    public static void main(String[] args) throws InterruptedException {
        final  VolatileTest01 test01 = new VolatileTest01();
        for (int n = 0; n < 1000; n++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    test01.addI();
                }
            }).start();
        }
        Thread.sleep(10000);// Wait 10 seconds to ensure that the above program is finishedSystem.out.println(test01.i); }}Copy the code

The results show that volatile does not guarantee atomicity

The i++ operation can be divided into three steps:

  • Read the value of I and load it into working memory
  • Add 1 to I
  • Write the value of I back to working memory and refresh to main memory

We know that the execution of thread has randomness, suppose that the working memory of thread A and thread B are both num=0, line A preempts the execution right of THE CPU, and adds 1 to the working memory, but has not been refreshed to main memory. * * * * * * * * * * * * * * * * * * * * * * Then thread A flushs to main memory where num=1, and thread B flushs to main memory where num=1, but num should be equal to 2 after two operations.

2.3.2 Why use volatile to share longs and Doubles?

becauselonganddoubleOperations for both data types can be divided into high 32-bit and low 32-bit parts, and therefore plainlongordoubleType read/write may not be atomic. Therefore, encourage everyone will sharelonganddoubleSet the variable tovolatileType, which is guaranteed to be correct in any caselonganddoubleEach read/write operation is atomic.

Iii. Hardware system architecture

When the computer is running the program, every instruction is executed in the CPU, in the process of program execution will inevitably involve data read and write operation. The data that programs run on the CPU is stored in main memory, and the following problems may occur: A CPU can execute instructions much faster than it can read data from main memory. If all the data used by a program needs to be read from main memory, the CPU will not be able to execute as efficiently as it can.

  • To solve the above problems, there isCPUCache for each cache for eachCPUUniquely, each cache holds the correspondingCPUExecute instructions and required data, soCPUInstead of working directly with main memory, you work with a cache that is much faster than main memory to get the most out of itCPUExecution efficiency.
  • CPUIn order to improve the efficiency of accessing data in eachCPUThere will be multiple levels of small but extremely fast caches on the core. In the cacheCache lineIs stored in units, whenCPUWhen a read memory instruction is executed, the contents of the cache line in which the memory address is located are loadedCPUIn the cache (load the entire cache row at once)
  • Cache enabledCPUThe reading speed has been greatly improved. Because the cache is distributed in differentCPUIn, therefore mustTo ensure aCPUAfter writing the data in eachCPUThe data stored in the cache is consistent.

The following two methods are used to ensure data consistency:

(1) direct writing. To write data directly from the local cache to the next level cache or main memory. If the corresponding data is cached, the contents of the cache are either directly updated or discarded.

(2) write back. The cache does not immediately write to the next level cache, but only modifies the data in the local level cache and marks the corresponding cache data as “dirty”. “Dirty” data triggers write back, writing its contents to the next level cache or corresponding main memory, so that data consistency can be ensured.

Diagram of interaction between processor, cache, and main memory:

Iv. Java Memory model

Java Memory Model, or JMM. The JMM is a concept, a convention, and does not exist in reality.

  • The working memoryandMain memoryThere are eight kinds of interactions:

1. Lock: A variable that acts on main memory, identifying a variable as thread-exclusive.

2. Unlock: A variable that acts on the main memory. It releases a variable that has been locked before being locked by another thread.

3. Read: Acts on main memory variables to transfer the value of a variable from main memory to the thread’s working memory for subsequent load operations.

4. Load: A variable that acts on the working memory. It puts the variable that the read operation obtains from main memory into the working memory.

5. Use: Works on variables in working memory. It transfers the variables from working memory to the execution engine and uses this instruction whenever the virtual machine accesses the value of a variable that needs to be used.

6. Assign: Acts on a variable in working memory. It assigns a value received from the execution engine to a copy of the variable in working memory.

7. Store: A variable in main memory that transfers the value of a variable in working memory to main memory for subsequent write use.

8. Write: A variable in main memory. It places the value of the variable obtained from the working memory by the store operation into the main memory variable.

The Java memory model also specifies that the following rules must be met when performing the eight basic operations described above:

  • One of the read and load, store, and write operations is not allowed separately
  • A thread is not allowed to discard its most recent assign operation, that is, variables changed in working memory must be synchronized to main memory.
  • A thread is not allowed to synchronize data from working memory back to main memory for no reason (no assign operation has occurred).
  • A new variable can only be created in main memory. It is not allowed to use an uninitialized (load or assign) variable in working memory. The use and store operations must be performed before the assign and load operations can be performed.
  • Only one thread can lock a variable at a time. Lock and unlock must be paired
  • If you lock a variable, the value of the variable will be cleared from the working memory, and you will need to load or assign the variable again to initialize it before the execution engine can use it
  • Unlock is not allowed if a variable has not been locked by the lock operation. It is also not allowed to unlock a variable that is locked by another thread.
  • Before you can unlock a variable, you must synchronize it to main memory (store and write).

The Java memory model defines an abstract relationship between threads and main memory as follows:

  • Shared variables are stored in main memory and can be accessed by each thread.
  • Each thread has private working memory and local memory.
  • The working memory value stores the thread’s copy of the shared variable.
  • Threads cannot directly operate on main memory, and can only write to main memory after first operating on working memory.
  • Working memory, like the Java memory model, is an abstract concept that doesn’t really exist. It covers caching, registers, compilation optimization, hardware, and so on.

Interactive diagram

Fifth, the implementation principle of volatile

5.1 Volatile visibility implementation

Memory visibility for volatile variables is achieved through Memory barriers:

  • A memory barrier, also known as a memory barrier, is a CPU instruction.

  • When program is running, in order to improve the performance, the compiler and processor will to reordering of instructions, JMM on different compilers and CPU in order to ensure that have the same results, by inserting a specific type of memory barrier to prohibit + a specific type of compiler to sorting and processing highly sorting, insert a memory barrier will tell the compiler and CPU: No instruction can be reordered with this memory-barrier instruction.

public class Test {
    private volatile int a;
    public void update() {
        a = 1;
    }
    public static void main(String[] args) {
        Test test = newTest(); test.update(); }}Copy the code

Compiled assembly code is available via hsDIS and jitWatch tools:

.0x0000000002951563: and    $0xffffffffffffff87,%rdi
  0x0000000002951567: je     0x00000000029515f8
  0x000000000295156d: test   $0x7,%rdi
  0x0000000002951574: jne    0x00000000029515bd
  0x0000000002951576: test   $0x300,%rdi
  0x000000000295157d: jne    0x000000000295159c
  0x000000000295157f: and    $0x37f,%rax
  0x0000000002951586: mov    %rax,%rdi
  0x0000000002951589: or     %r15,%rdi
  0x000000000295158c: lock cmpxchg %rdi,(%rdx)  // Write to volatile shared variables with the lock prefix
  0x0000000002951591: jne    0x0000000002951a15
  0x0000000002951597: jmpq   0x00000000029515f8
  0x000000000295159c: mov    0x8(%rdx),%edi
  0x000000000295159f: shl    $0x3,%rdi
  0x00000000029515a3: mov    0xa8(%rdi),%rdi
  0x00000000029515aa: or     %r15,%rdi
......
Copy the code

The lock prefix instruction causes two things on multicore processors:

  • Writes data from the current processor cache row back to system memory.
  • The operation of writing back to memory will be made in otherCPUInvalid data cached in the memory address.

To increase processing speed, instead of directly communicating with memory, the processor reads system memory data into an internal cache (L1, L2, or other) before operating, but does not know when the data will be written to memory.

If a volatile variable is written, the JVM sends an instruction prefixed with “LOCK” to the processor to write the variable’s cached line back to system memory.

In order to ensure that each processor cache is consistent, the cache coherence protocol (msci), 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 modifies the data, it reads the data back from system memory to the processor cache.

This is also done on all multicore processors: when the processor finds that the local cache is invalid, it will re-read the variable data from memory, which means it can get the latest value.

Volatile variables have a mechanism by which each thread can obtain the latest value of the variable.

5.2 Volatile ordering implementation

Volatile disallows reordering

The FOUR categories of JMM memory barriers are shown in the following figure

The Java compiler inserts memory barrier instructions in place when generating the instruction series to prevent reordering of certain types of processors. In order to implement the memory semantics of volatile, the JMM will restrict the reordering of certain types of compilers and processors. The JMM will specify a volatile reordering table for compilers:

“NO” means that reordering is forbidden. To implement volatile memory semantics, when the compiler generates bytecode, it inserts a memory barrier in the instruction sequence to prevent reordering of certain types of processors. It is almost impossible for the compiler to find an optimal placement that minimizes the total number of insertion barriers, so the JMM takes a conservative approach:

  1. At the end of eachvolatilewritesIn front of theInsert a StoreStore barrier;
  2. At the end of eachvolatilewritesbehindInsert a StoreLoad barrier;
  3. At the end of eachvolatileThe read operationbehindInsert a LoadLoad barrier;
  4. At the end of eachvolatileThe read operationbehindInsert a LoadStore barrier.

Note that volatile writes insert memory barriers before and after, while volatile reads insert two memory barriers after

StoreStore barrier: prohibits reordering of normal writes above and volatile writes below;

StoreLoad barrier: Prevents volatile reads above from being reordered from volatile reads/writes below

LoadLoad barrier: disables all normal read operations below and volatile read reordering above

LoadStore barrier: disallows all normal write operations below and volatile read reorder above

Vi. Application Scenarios of Volatile

Conditions for using volatile

  • A write to a variable does not depend on the current value.
  • The variable is not included in an invariant with other variables.
  • Only when the state is truly independent of the rest of the programvolatile.

Mode 1: Low-overhead read-write lock policy

@ThreadSafe
public class CheesyCounter {
    // Employs the cheap read-write lock trick
    // All mutative operations MUST be done with the 'this' lock held
    @GuardedBy("this") private volatile int value;
 
    public int getValue() { return value; }
 
    public synchronized int increment() {
        returnvalue++; }}Copy the code

Mode 2: Double-checked

class Singleton {
    private volatile static Singleton instance;
    private Singleton() {
    }
    public static Singleton getInstance() {
        if (instance == null) {
            syschronized(Singleton.class) {
                if (instance == null) {
                    instance = newSingleton(); }}}returninstance; }}Copy the code

reference

The Art of Concurrent Programming in Java

Java Interview To the End – The Basics

Understanding the Java Virtual Machine

The use of volatile and its principles