12 Atomicity|Visibility|Ordering|JMM memory model

Table of Contents

1 Three major characteristics of concurrency

1.1 Atomicity

1.2 Visibility

1.3 Orderliness

2 Java Memory Model JMM

2.1 Abstract structure of JMM

2.2 Main memory and working memory interaction protocol

2.3 Memory semantics of locks

2.4 volatile memory semantics

2.4.1 Semantics of volatile writing

2.4.2 Semantics of volatile read

2.4.3 Implementation principle of volatile memory semantics


1 Three major characteristics of concurrency

1.1 Atomicity

One or more operations, either all of them are executed or none of them are executed. In Java, reading and assigning operations to variables of basic data types are atomic operations, but increment operations that do not take any atomic guarantee measures are not atomic, such as: i + +

public class AtomicTest {

    static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i + + ) {
            new Thread(()->{
                for (int j = 0; j < 1000; j + + ) {
                    count + + ;
                }
            }).start();
        }

        Thread.sleep(2000);
        System.out.println(count);
    }
}

The above results are inconsistent every time it is executed, indicating that a thread safety issue has occurred.

How to ensure atomicity?

sychronized, lock, CAS (AtomicInteger)

1.2 Visibility

Multiple threads access the same variable, and if one thread modifies it, other threads can detect it in time.

How to ensure visibility?

volatile, memory barrier, sychronized, lock

1.3 Orderliness

The order of program execution is based on the order of code (in order to improve performance, compilers and processors often reorder instructions, which may cause ordering problems)

Java orderliness relies on memory barriers

How to ensure orderliness?

volatile, memory barrier, sychronized, lock

2 Java Memory Model JMM

Problems that need to be solved in concurrent programming:

1 How to communicate between multiple threads (what mechanism is used to exchange data between threads)

2 How to synchronize between multiple threads (the relative order in which operations occur between different threads)

2.1 Abstract structure of JMM

JMM determines when one thread’s writes to a shared variable are visible to another thread

JMM defines the abstract relationship between threads and main memory

1 Shared variables exist in main memory

2 Each thread has its own private local memory

3 Store a copy of the shared variable locally

4 All operations on shared variables by threads must be performed in local memory and cannot directly read main memory.

If thread A and thread B want to communicate, they must go through the following two steps:

Thread A updates the shared variables updated by local memory A to the main memory.

Thread B reads the shared variables updated by thread A from main memory.

Note: Thread A cannot directly access the working memory of thread B. Inter-thread communication must go through main memory

2.2 Main memory and working memory interaction protocol

Eight atomic operations:

lock: Acts on main memory, marking a variable as thread exclusive (main memory)

unlock: Release variables in the exclusive state of main memory (main memory)

read: main memory variables are transferred to the worker thread (main memory)

load: Put the variables obtained by the read operation into the variable copy of the working memory (working memory)

use: Variables in working memory are passed to the execution engine (working memory)

assgin: The value obtained from the storage engine is assigned to the variable of the working memory (working memory)

store: a variable value from working memory is transferred to main memory (working memory)

Write: Put the store operation from the variable value of the working memory to the main memory variable (main memory)

Eight rules that must be met for atomic operations:

1. To copy a variable from main memory to working memory, you must follow the read and load operations in sequence; to synchronize a variable from the working memory back to the main memory, you must follow the store and write operations in sequence.

2 The read load store write operation is not allowed to appear alone

3 A thread is not allowed to discard assgin. Variables must be synchronized to main memory after being changed in memory.

4. A thread is not allowed to synchronize from working memory to main memory without assgin occurring.

5 A new variable can only be created in main memory, and an uninitialized variable is not allowed to be used in working memory.

6 A variable is only allowed to be locked by one thread at the same time, but it can be executed multiple times by the same thread, but it also needs to perform the same number of unlock operations.

7 If a lock operation is performed on a variable, the value of the variable in the working memory will be cleared (the execution engine must reload or assign to use this variable)

8 Unlocking a variable that is not locked is not allowed

9 Before executing unlock on a variable, the variable must be synchronized to the main memory (execute store and write)

Visibility issues arise:

Thread B’s modification of the variable flag will not be perceived by thread A. Only when thread B locks the main memory shared variable flag, thread A will retrieve it from the main memory again.

There are two ways to implement the underlying visibility in Java:

1 Memory barrier (sychronized, Thread.sleep(10), volatile)

2 CPU context switching (Thread.yield(), Thread.sleep())

2.3 Memory Semantics of Locks

Memory semantics of lock release and lock release:

When a thread acquires a lock: JMM invalidates the local memory corresponding to the thread

When a thread acquires the lock: JMM refreshes the variables in the local memory corresponding to the thread into the main memory.

The function of the sychronized keyword is to ensure mutual exclusion and visibility when multiple threads access shared resources; before acquiring the lock, the thread will transfer the latest value of the shared variable from main memory -> Working memory, after the lock is released the modified value will be flushed into the main memory to ensure visibility

2.4 volatile memory semantics

2.4.1 Semantics of volatile writing

When writing a volatile variable, JMM will refresh the shared variable in the local memory corresponding to the thread to the main memory.

public static void main(String[] args) throws InterruptedException {
    for (int i = 0; i < 10; i + + ) {
        new Thread(() -> {
            for (int j = 0; j < 10000; j + + ) {
                count + + ; //static volatile int count = 0;
            }
        }).start();
    }

    Thread.sleep(10000);
    System.out.println(count);
}

The above results will be different. The reason is: when the variable is used in the execution engine, volatile cannot change the value in the execution engine. When the variable is assigned back to the working memory in the execution engine, overwriting will occur.

2.4.2 Semantics of volatile read

When reading a volatile variable, JMM will invalidate the local memory corresponding to the thread, and the shared variable needs to be read from the main memory (volatile reading can ensure that each read is the latest data)

2.4.3 Implementation Principle of Volatile Memory Semantics

Disable command reordering:

When the second operation is a volatile write, no matter what the first operation is, it cannot be reordered.

When the first operation is a volatile read, no matter what the second operation is, it cannot be reordered.

When the first operation is a volatile write and the second operation is a volatile read, reordering cannot be performed.

Example:

private static Singleton singleton;
/**
 * Double-checked Locking implements delayed initialization of singleton objects
 *
 * @return
 */
public static Singleton getSingleton() {
    if (singleton == null) {
        synchronized (Singleton.class) {
            The correct usage should be to use volatile to modify singleton
            The reason is that the line of code singleton = new Singleton() creates an object. This line of code can be decomposed into three lines of pseudo code. The above 2 and 3 may be reordered. The execution timing after the reordering is as follows: In order to achieve volatile memory semantics, when the compiler generates bytecode, it will be in the instruction sequence. Insert memory barriers to prohibit specific types
            processor reordering. JMM memory barrier insertion strategy:
            1. Insert a StoreStore barrier in front of each volatile write operation
            2. Insert a StoreLoad barrier after each volatile write operation
            3. Insert a LoadLoad barrier after each volatile read operation
            4. Insert a LoadStore barrier after each volatile read operation
            The above memory barrier insertion strategy is very conservative, but it can ensure that correct results can be obtained in any program on any processor platform.
            Volatile memory semantics.
            if (singleton == null) {
                singleton = new Singleton();
            }
        }
    }
    return singleton;
}

The volatile modification should be added, because the new object is divided into three steps and its atomicity cannot be guaranteed:

private volatile static Singleton singleton;

The above three steps can be completely rearranged into:

The knowledge points of the article match the official knowledge files, and you can further learn related knowledge. Java Skill TreeHomepageOverview 138586 people are learning the system