Java Memory Model (JMM)

What is the JMM?

The Java Memory model is an abstract concept that describes a set of rules or specifications that define how variables in a program (including instance fields, static fields, and elements that make up array objects) can be accessed. The JVM’s running entity is a thread, and each thread is created with a working memory (called stack space in some places) that is used to store data that is private to the thread. The Java memory model dictates that all variables are stored in main memory, which is a shared memory area accessible to all threads. But threads (reading assignment, etc.) to the operation of the variables must be conducted in the working memory, the first thing to copy the variables from the main memory of yourself working memory space, and operation, the variable operation to complete before you write variables back to main memory, can’t direct operation The main variables in memory, working memory stored in main memory copy of the variables in the copy, Working memory is the private data area of each thread, so different threads cannot access each other’s working memory. Communication between threads (passing values) must be done through main memory.

The JMM differs from the JVM memory region model

JMM in main memory, working memory and the JVM the Java heap, stack, and method of area is not the same level of memory, both of which are basically no relationship, if both must force corresponding to that from the definition of variables, main memory, working memory, main memory is mainly corresponding to the object instance data portion of the Java heap, Working memory corresponds to a portion of the virtual machine stack. At a lower level, main memory corresponds directly to physical hardware memory, and a virtual machine (or even an optimization of the hardware system itself) may prioritize working memory in registers and caches for better performance, since it is working memory that the program primarily accesses and writes.

Why is the JMM needed

In order to ensure the correctness (visibility, orderliness, atomicity) of shared memory, the memory model defines the specification of read and write operation of multithreaded program in shared memory system. These rules are used to regulate the read and write operations of memory, so as to ensure the correctness of instruction execution. It’s about the processor, it’s about the cache, it’s about concurrency, it’s about the compiler. It solves the memory access problems caused by multi-level CPU cache, processor optimization and instruction rearrangement, and ensures the consistency, atomicity and order in concurrent scenarios.

The JMM is a specification designed to address issues such as inconsistencies in local memory data, compiler reordering of code instructions, and out-of-order code execution by processors when multiple threads communicate through shared memory.

Why do you need multithreaded communication

The speed of CPU, memory and I/O devices is greatly different. In order to make reasonable use of the high performance of CPU and balance the speed difference of these three, computer architecture, operating system and compiler have made contributions, which are mainly reflected as follows:

  • The CPU added a cache to balance the speed difference with memory. // Causes visibility problems

  • The operating system added processes and threads to time-share multiplexing CPU, and then balance the speed difference between CPU and I/O device; // Causes atomicity problems

  • The compiler optimizes the order of instruction execution so that the cache can be used more efficiently. // Result in orderliness issues

Multithreaded communication leads to concurrency, the root of concurrency

Visibility (caused by CPU caching)

Changes made by one thread to a shared variable are immediately visible to another thread.

CPU cache consistency problem
// Thread 1 executes the code
int i = 0;
i = 10;
 
// Thread 2 executes the code
j = i;
Copy the code

If thread 1 is executing CPU1, thread 2 is executing CPU2. If thread 1 executes “I =10”, the initial value of “I” is loaded into CPU1’s cache and then assigned to “10”, then the value of “I” in CPU1’s cache becomes “10”, but is not immediately written to main memory.

If thread 2 executes j = I, it will fetch the value of I from main memory and load it into CPU2’s cache. Note that the value of I in memory is still 0, so j will be 0 instead of 10.

Solutions to cache inconsistencies:
  • Bus locking is pessimistic and inefficient, blocking access to other components by other cpus

  • Intel’s MESI cache consistency protocol:

    MESI is an acronym for the four cache segment states in which a cache segment in any multicore system resides.

    A cache segment that is either no longer in the cache or whose contents are obsolete. Segments in this state are ignored for caching purposes. Once a cache segment is marked as invalid, the effect is the same as if it was never loaded into the cache.

    A Shared cache segment is a copy of the main memory and can only be read, not written. Multiple groups of caches can have a shared cache segment for the same memory address at the same time, hence the name.

    An Exclusive cache segment, like the S state, is a copy of the contents of the main memory. The difference is that if one processor holds a cache segment of E state, no other processor can hold it at the same time, so it is called “exclusive.” This means that if other processors originally hold the same cache segment, it will immediately become “invalidated.”

    Cache segments that are dirty have been Modified by the processor to which they belong. If a segment is in the modified state, its copy in other processors’ caches will immediately become invalid, just as in the E state. In addition, if a modified cache segment is discarded or marked as invalidated, its contents are written back to memory — just as normal dirty segments are handled in write back mode.

    If the CPU is operating on the data in the Cache and the variable is a shared variable, that is, a copy exists in the Cache of another CPU, then: (1) Read operation, do not do any processing, only read data from the Cache into the register. (2) Write operation, send a signal to inform other cpus to set the variable Cache Line to invalid state, other cpus must read the variable in the main memory again.

  • The volatile keyword ensures visibility.

If the MESI protocol is used to ensure cache consistency, why do we need volatile to ensure visibility

MESI only ensures consistency between the exclusive caches of multi-core cpus, but cpus do not write data directly to L1 caches, and may also have store buffers in between. Some ARM and Power cpus may also have load buffers or invalid queues. At best, the MESI protocol guarantees read and write order for a variable on multiple cores, but there is no guarantee for multiple variables.

Atomicity (due to CPU time-sharing multiplexing)

An operation or operations, either all performed without interruption by any factor, or none performed at all. The Java memory model only guarantees that basic reads and assignments are atomic operations, and that atomicity for broader operations can be achieved through synchronized and Lock. Since synchronized and Lock guarantee that only one thread executes the code block at any one time, atomicity is naturally eliminated and thus guaranteed.

Orderliness (compiler and processor optimally ordering instructions)

  • The order in which the program is executed is the order in which the code is executed.

  • The volatile, synchronized, and Lock keywords ensure visibility.

  • The JMM ensures orderliness through the happens-before rule

What volatile does

Volatile is a lightweight synchronization mechanism provided by the Java Virtual machine.

  • The shared variable that is volatile is guaranteed to be visible to the total number of threads, i.e., when a thread changes

    The value of a shared variable is volatile, and the new value is always immediately known by other threads.

  • Disallow instruction reordering optimization.

Prevents command reordering

The as – if – serial semantics

The as-if-serial semantics mean that the execution result of a (single-threaded) program cannot be changed, no matter how much reordering is done (to improve parallelism by the compiler and processor). The compiler, runtime, and processor must comply with the AS-IF-Serial semantics

Below is a schematic diagram of the sequence of instructions from source to final execution:

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

Instantiating an object can be divided into three steps:

1. Allocate memory space.

2. Initialize the object.

3. Assign the address of the memory space to the reference.

However, since the operating system can reorder instructions, the above procedure may also become the following:

1. Allocate memory space.

2. Assign the address of the memory space to the reference.

3. Initialize the object

A multithreaded environment can expose an uninitialized object reference, leading to unexpected results. Therefore, to prevent reordering of this process, we need to set the variable to volatile.

package com.test.thread;

/ * * *@author SSH
 * @date2021/9/1 * /
public class OrderlinessTest {

    private static int x = 0;
    private static int y = 0;
    private static int a = 0;
    private static int b = 0;

    public static void main(String[] args) throws InterruptedException {
        int i = 0;
        for(; ;) { i++; x =0;
            y = 0;
            a = 0;
            b = 0;
            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run(a) {
                    try {
                        Thread.sleep(10);
                        a = 1;
                        x = b;
                    } catch(InterruptedException e) { e.printStackTrace(); }}}); Thread t2 =new Thread(new Runnable() {
                @Override
                public void run(a) {
                    try {
                        Thread.sleep(10);
                        b = 1;
                        y = a;
                    } catch(InterruptedException e) { e.printStackTrace(); }}}); t1.start(); t2.start(); t1.join(); t2.join(); String result ="The first" + i + "(" + x + "," + y + ")";
             System.out.println(result);
            if (x == 0 && y == 0) {
                break; }}}}Copy the code
The first320Time (1.0) the first321Time (1.0) the first322Time (0.1) the first323Time (1.0) the first324Time (0.1) the first325Time (0.1) the first326Time (0.1) the first327Time (1.0) the first328Time (1.0) the first329Time (0.1) the first330Time (1.0) the first331Time (1.0) the first332Time (1.0) the first333Time (1.0) the first334Time (0.0)
Copy the code

(0,0) occurs because of an instruction rearrangement

Implement visibility

package com.test.thread;

import lombok.extern.slf4j.Slf4j;

/ * * *@author s
 * @date2021/9/1 * /
@Slf4j
public class VisibilityTest {
    private static boolean initFlag = false;
    private static int count = 0;

    public static void refresh(a) {
        log.info("refresh data.........");
        initFlag = true;
        log.info("refresh data success.........");
    }

    public static void main(String[] args) {

        Thread threadA = new Thread(() -> {
            while(! initFlag) { } log.info("Thread" + Thread.currentThread().getName() + "Current thread sniffs initFlag state change");
        }, "Thread A");
        threadA.start();

        try {
            Thread.sleep(1000);
        } catch (Exception e) {
            e.printStackTrace();
        }

        Thread threadB = newThread(() -> { refresh(); }); threadB.start(); }}Copy the code

Log.info (” Thread “+ thread.currentthread ().getname () +” currentThread sniffing change of initFlag state “); This sentence will not be printed.

Metaphysical phenomena:

If,

private static int count = 0;
Copy the code

to

private static Integercount = 0;
 while(! initFlag) { count++; }Copy the code

or

 Thread threadA = new Thread(() -> {
            while(! initFlag) { System.out.println("running");
            }
            log.info("Thread" + Thread.currentThread().getName() + "Current thread sniffs initFlag state change");
        }, "Thread A");
        threadA.start();
Copy the code

Log.info (” Thread “+ thread.currentthread ().getname () +” currentThread sniffing change of initFlag state “); The sentence will be printed.

Ensure atomicity: single read/write

! [java-thread-x-key-volatile-3](C:\Users\lenovo\Desktop\java-thread-x-key-volatile-3.png)public class VolatileTest01 {
    volatile int i;

    public void addI(a){
        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(a) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    test01.addI();
                }
            }).start();
        }
        Thread.sleep(10000);// Wait 10 seconds for the above program to completeSystem.out.println(test01.i); }}Copy the code

The result is less than 1000 because I ++ is not an atomic operation, which involves reading the value of I, incrementing I by one, writing the value of I back to memory.

Orderliness is achieved by the happens-before principle

  1. The principle of program sequence, that is, semantic serialization must be guaranteed within a thread, that is, the execution of code in order.

  2. An unlock operation must occur before a lock is added to the same lock. That is, if a lock is added after a lock is unlocked, the action to add the lock must follow the action to unlock the same lock.

  3. The rule for volatile variables is that writes of volatile variables occur before reads. This ensures visibility of volatile variables. In short, a volatile variable forces the value of the variable to be read from main memory each time it is accessed by a thread. At any time, different threads can always see the latest value of the variable.

  4. A thread starts A rule thread’s start() method before each of its actions, that is, if thread A modifies the value of A shared variable before executing thread B’s start method, the changes made by thread A to the shared variable are visible to thread B when thread B executes the start method

  5. Transitivity A precedes B, B precedes C so A must precede C

  6. Thread termination rules All operations on a Thread precede its termination. The purpose of the thread.join () method is to wait for the currently executing Thread to terminate. Suppose that the shared variable is modified before thread B terminates. After thread A successfully returns from thread B’s join method, thread B’s changes to the shared variable will be visible to thread A.

  7. Thread.interrupted() calls to the threadinterrupt () method occur when code on the interrupted Thread detects that an interrupt event has occurred.

  8. Object finalization rule An object’s constructor executes, finalizing before the Finalize () method

Memory visibility for volatile variables is implemented based on 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 instructions can be reordered with this memory-barrier instruction.

  • 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 memory barrier instructions
StoreStore barrier Disallow normal writes above and volatile write reordering below.
StoreLoad barrier Prevents volatile writes above from potentially volatile read/write reordering below.
LoadLoad barrier Disallow all normal reads below and volatile read reordering above.
LoadStore barrier Disallow all normal writes below and volatile read reordering above.