During an interview, many interviewers like to ask, is JMM clear? What is memory visibility, what is reordering? Principles in synchronized, volatile, and final? And so on and so forth. And a search on the Internet, ba la la a lot of things more messy, it is difficult to disguised questions to answer the interviewer clearly. Finally, I decided to give you a simplified version of the JAVA memory model.

1. Introduction.

Concurrency in Java is a shared memory model, where communication between Java threads is always implicit and completely transparent to the programmer.

2. Brief introduction of JMM.

The JMM is the Java memory model that determines when a write to a shared variable by one thread is visible to another thread. From an abstract perspective, JMM defines an abstract relationship between threads and Main Memory: Shared variables between threads are stored in Main Memory, and each thread has an abstract (non-existent) Local Memory that stores copies of shared variables that the thread reads and writes.

The JMM’s “shared variables” are mostly in the Java heap.

The JVM memory model includes: (1) program counters. A small area of memory used to record an instruction to be run. Is thread private memory. (2) Java virtual machine stack. It is created at the same time as a Java thread, holds local variables, partial results, and participates in method calls and returns. Is thread private memory. (3) Local method stack. It functions similarly to the Java virtual machine stack and primarily serves Native methods. Is thread private memory. (4) the Java heap. Allocates memory for all created objects and arrays. Is memory shared by threads. (5) Method area. Also known as the permanent region, similar to the heap space. Is memory shared by threads.Copy the code

3. Reordering in JMM.

When executing a program, the compiler and processor often reorder instructions to improve performance. However, before the program is finally executed, there is a memory reorder to be done.

Reordering can cause memory visibility problems in multithreaded programs. Look at the code example:

  class ReorderEample {
        int a = 0;
        boolean flag = false;
        / / write operations
        public void writer(a) {
            a = 1 ;  / / (1)
            flag = true; / / (2)
        }
        / / read operation
        public void reader(a) {
            if (flag) {    / / (3)
                int i = a * a;  / / (4)
                // Process logic}}}Copy the code

The flag variable is a flag that indicates whether a has been written. Suppose there are two threads, A and B. A first performs the writer() operation, and B then performs the Reader () method. Can thread B perform operation (4) and see thread A write to the shared variable A in operation (1)? The answer is not necessarily. Because during the reorder, thread A may have identified flag variables before writing to variable A, but thread B now reads them before they happen, the semantics of the program are broken. The following program execution sequence diagram:

Memory semantics for volatile

In the JMM, a shared variable declared as volatile is acquired by the thread through an exclusive lock to ensure that it is visible to the thread. To implement the memory semantics of volatile, when the bytecode is generated, the compiler inserts a memory barrier into the instruction sequence to prevent a particular type of handler from reordering. See the Java Memory Model Cookbook for details on memory barriers

4.1. Characteristics of Volatile

  • Visibility. A read of a volatile variable always sees the last write to that volatile variable (by any thread).
  • Atomicity. Atomicity of read/write to any single volatile variable. However, for multiple volatile operations or volatile++ the compound operation is not atomic.

4.2. Volatile Solves the reordering problem

Volatile follows the happens-before principle. See the code below:

class ReorderEample {
        int a = 0;
        volatile boolean flag = false;
        / / write operations
        public void writer(a) {
            a = 1 ;  / / (1)
            flag = true; / / (2)
        }
        / / read operation
        public void reader(a) {
            if (flag) {    / / (3)
                int i = a * a;  / / (4)
                // Process logic}}}Copy the code

Suppose that after thread A executes writer(), thread B executes reader(). According to the happens-before principle, the happens-before relationships established by this process fall into three categories:

    1. (1) happens-before (2); (3) happens-before (4).
    1. According to the volatile rule, (2) happens-before (3).
    1. According to the transitive rule of happens-before, (1) happens-before (4).

Its happens-before relationship is established as follows:

Happens-before rule

(1) The program order rule: every action in a thread is happens-before any subsequent action in that thread. (2) The monitor lock rule: unlock a thread, happens-before any subsequent lock in that thread. (3) The volatile variable rule: Writes to a volatile field happens-before subsequent reads to the volatile field (4) transitivity: If A happens-before B, and B happens-before C, then A happens-before C (5)start() If thread A performs an operation ThreadB_start()(starts thread B), then thread A's ThreadB_start()happens before any operation in thread B. If A executes threadb.join () and returns successfully, then any action in ThreadB returns before thread A returns successfully from threadb.join (). Interrupt () principle: The interrupt() method is invoked when the code of the interrupted Thread detects the occurrence of an interrupt event. If thread.interrupt () is used, finalize() can be used to check whether an interrupt has occurred. The completion of an object's initialization occurs first at the start of its Finalize () method.Copy the code

Synchronized memory semantics

Synchronized memory semantics are similar to volatile memory semantics. In Java concurrent programming mechanisms, locking allows the thread releasing the lock to send a message to the thread acquiring the same lock, in addition to making critical sections mutually exclusive. At its core is the underlying use of a volatile declared state variable to maintain synchronization state.

5.1 Synchronized solves the reordering problem

Locks also follow the happens-before rule. See the code below:

  class MonitorExample{
        int a = 0;
        / / write operations
        public synchronized void writer(a) { / / (1)
            a ++;                           / / (2)
        }                                   / / (3)
        / / read operation
        public synchronized void reader(a) { / / (4)
            int i = a;                      / / (5)
            // Process logic
        }                                   / / (6)
    }
Copy the code

Suppose thread A executes the writer() method, followed by thread B which executes the Reader () method. According to the happens-before rule, the happens-before relationships involved in this process can be divided into three categories:

    1. According to the order of procedure, (1) happens-before (2), (2) happens-before (3), (4) happens-before (5), (5) happens-before (6).
    1. According to the monitor lock rule, (3) happens-before (4).
    1. According to the transitive rule, (2) happens-before (5).

Its happens-before relationship is established as follows:

6. Final memory semantics

In the JMM, the memory barrier prevents the compiler from reordering the writes of final fields out of the constructor. Thus, the final field of the object is properly initialized (not null) before the object reference is visible to any thread. For final domains, the compiler and processor follow two reordering rules:

  • There is no reordering between a write to a final field within a constructor and a subsequent assignment of a reference to the constructed object to a reference variable.
  • There is no reordering between the first reading of an object containing a final field and the subsequent first reading of the final field.

Here are two examples to illustrate these two rules.

6.1. Example 1

public class FinalExample {
    int i;                           // Common variables
    final int j;                     / / final variables
    static FinalExample obj;
    public FinalExample(int j) {     // constructor
        i = 1;                       // Write the normal field
        this.j = j;                  / / write final domain
    }
    
    public static void writer(a) {    // Write thread A executes
        obj = new FinalExample(2);
    }
    
    public static void reader(a) {    // Read-thread B executes
        FinalExample object = obj;   // Read the reference object
        int a = object.i;            // Read the normal field
        int b = object.j;            / / read the final domain}}Copy the code

The writes to the common field are reordered by the compiler outside the constructor, and reader thread B mistakenly reads the value of the common variable I before it is initialized. After writing to the final field, the reordering rules written to the final field are “restricted” to the constructor, and reader THREAD B correctly reads the initialized value of the final variable. The execution sequence diagram is as follows:

6.2 example 2

public class FinalReferenceExample {
    final int[] intArray;
    static FinalReferenceExample obj;

    public FinalReferenceExample(a) {   // constructor
        intArray = new int[1];         / / (1)
        intArray[0] = 1;               / / (2)
    }

    public static void writeOne(a) {         // Write thread A executes
        obj = new FinalReferenceExample();  / / (3)
    }

    public static void writeTwo(a) {         // Write thread B executes
        obj.intArray[0] = 2;                / / (4)
    }

    public static void reader (a) {         // Read-thread C executes
        if(obj ! =null) {                 / / (5)
            int temp = obj.intArray[0];    / / (6)}}}Copy the code

First thread A executes the writeOne() method, then thread B executes the writeTwo method, and then thread C executes the Reader method. Operation (1) writes to a final field, operation (2) writes to a member of an object referenced by a final field, and operation (3) assigns a reference to the constructed object to a reference variable. There’s no reordering except for (1) and (3), and there’s no reordering of (2) and (3). Therefore, the execution timing of the program’s threads is unknown because there is a data race between writer thread B and reader thread C.

Reference: “Java concurrent programming art” Fang Tengfei, Wei Peng, Cheng Xiaoming, “In-depth understanding of Java Virtual machine” Zhou Zhiming

If this article is useful to you, please give it a thumbs up! Your support is my motivation to share.