I. Fundamentals of the Java memory model
Two key issues in the 1.1 concurrent programming model
In concurrent programming, there are two key issues that need to be addressed: how to communicate between threads and how threads are synchronized (the threads here refer to the activity entities that execute concurrently). Communication refers to the mechanism by which the threads exchange information. In imperative programming, there are two kinds of communication mechanisms between threads: Shared memory and message passing.
In the shared memory model, the common state of the program is shared among threads, and implicit communication is made through the public state in the read-write memory. In the concurrency model of message passing, there is no public state between threads, and the threads must communicate by sending messages to display.
Synchronization refers to a mechanism in a program that controls the relative order of operations between different threads. In the shared memory concurrency model, synchronization is displayed. Programmers must show that specifying a method or a piece of code requires mutually exclusive execution between threads. In the concurrency model of message passing, synchronization is implicit because the message must be sent before the message is received.
Java concurrency is a shared-memory model, where communication between Java threads is always implicit and the entire communication process is completely transparent to the programmer. If a Java programmer writing a multithreaded program does not understand the working mechanism of implicit communication between threads, it is likely to encounter a variety of strange memory visibility issues.
1.2 Abstract structure of the Java memory model
In Java, all entity, static, and array elements are stored in heap memory and heap memory is shared between threads (this chapter uses the term "shared variables" to refer to instance fields, static fields, and array elements). Local variables, method definition parameters, and exception handler parameters are not shared between threads, they have no memory visibility issues, and are not affected by the memory model.
From an abstract point of view, the Java Memory Model (abbreviated JMM) defines the abstract relationship between threads and main memory: Shared variables between threads are stored in main memory, each thread has a private local memory, and local memory stores the thread to read/write a copy of the shared variable. Local memory is an abstract concept of JMM and is not really there. It covers caches, write buffers, registers, and other hardware and compiler optimizations.
Communication between two threads requires 2 steps: 1, thread A flushes the updated shared variables in local memory to main memory. 2. Thread B goes to main memory to read shared variables that have been updated before thread A.
1.3 Reordering from source code to instruction sequence
In order to improve performance, the compiler and processor often reorder the instructions when executing the program. There are 3 types of reordering.
1. Compiler-Optimized reordering. The compiler can rearrange the execution order of the statements without changing the single-line semantics.
2, command-level parallel re-ordering. Modern processors use instruction-level parallelism to overlap multiple instructions. If there is no data dependency, the processor can change the order in which the statement corresponds to the machine instruction execution.
3. Reorder the memory system. Because the processor swapped cache and read/write buffers, this makes loading and storage operations appear likely to be performed in a disorderly order.
From the source code to the final actual execution of the sequence of instructions, there are 3 kinds of reordering.
Source code >1: Compiler-Optimized reordering >2: instruction-level parallel reordering >3: Memory system reordering > final executed instruction sequence
The above 1 belongs to the compiler reordering, and 2 and 3 are processor reordering. These reordering may cause memory visibility issues with multithreaded programs. For compilers, the compiler-JMM of a compiler prevents a particular type of compiler from being reordered (not all compilers are forbidden). For processor reordering, the JMM collation of a handler requires the Java compiler to insert a specific type of memory barrier directive when generating a sequence of instructions, preventing a particular type of handler from being reordered by a memory barrier directive.
JMM is a language-level memory model that ensures that programmers are guaranteed consistent memory visibility over different compilers and different processor platforms by prohibiting certain types of compilers from reordering and processing.
1.4 Classification of concurrent programming models
Modern processors use write buffers to temporarily save data written to memory. Write buffers ensure that the instruction pipeline runs continuously, and it avoids the delay that occurs when the processor pauses to wait for the data to be written to the memory. At the same time, the memory bus is reduced by flushing the write buffers in batches and merging multiple writes of the same memory address in the write buffer. While there are so many benefits to write buffers, the write buffers on each processor are only visible to the processor on which it resides. This feature has an important impact on the order in which memory operations are executed: the order in which the processor performs read/write operations on memory is not necessarily the same as the order in which the memory actually occurs in the read/write operation!
Example: Processora:a=1; (A1) x=b; (A2). processorb:b=2; (B1) y=a; (B2). The initial state of a=b=0, assuming that processor A and processor B perform memory accesses in parallel in the order of the program, may eventually get x=y=0 results.
Analysis: Processor A and processor B can simultaneously write shared variables to their own buffers (A1,B1), then read another shared variable (A2,B2) from memory, and finally flush the dirty data stored in their write buffers into memory (A3,B3). When executed in this time series, the program can get x=y=0 results. In the order in which the memory operations actually occur, it is known that processor a executes A3 to refresh its write buffer, and the write operation A1 is actually executed. Although processor a performs memory operations in the order of: A1>A2, the actual order in which memory operations actually occur is A2>A1. At this point, the sequence of memory operations for processor A is reordered, as is the case with processor B. The key here is that because the write buffer is visible only to its own processor, it can cause the processor to perform memory operations in a sequence that may be inconsistent in the order in which the actual operation of the memory is performed. Because modern processors use write buffers, modern processors allow for the reordering of write-read operations.
To ensure memory visibility, the Java compiler inserts a memory barrier directive in place of the generated instruction sequence to suppress a particular type of handler reordering. JMM divides the memory barrier instruction into 4 categories:
Storeload barriers is an "all-in-one" barrier that simultaneously has the effect of 3 other barriers. Most modern processors support this barrier (other types of barriers are not necessarily supported by all processors). The overhead of executing the barrier is expensive because the current processor typically flushes all the data in the write buffer into memory.
1.5 Happens-before Introduction
Starting with JDK 5, Java uses the new JSR-133 memory model (unless otherwise noted, this article is for the JSR-133 memory model). JSR-133 uses the concept of Happens-before to illustrate the memory visibility between operations. In JMM, if the result of one operation needs to be visible to another operation, there must be a happens-before relationship between the two operations. The two actions mentioned here can be either within a thread or between different threads.
The Happens-before rules that are closely related to programmers are as follows:
Program Order rules: Each action in a thread is happens-before to any subsequent action in that thread.
Monitor lock rule: the unlocking of a lock is happens-before to the subsequent locking of the lock.
Volatile variable rule: writes to a volatile field, happens-before to any subsequent reading of this volatile field.
Transitivity: If a happens-before B, and B happens-before C, then a Happens-before c.
Note: There is a happens-before relationship between the two operations, which does not mean that the previous operation must be performed before the next operation! Happens-before only requires that the previous operation (the result of the execution) be visible to the latter operation, and that the previous operation precedes the second operation in order. The definition of Happens-before is subtle, and a happens-before rule corresponds to one or more compilers and handler reordering rules. For Java programmers, the Happens-before rules are easy to understand, and it avoids the Java Programmer Learning Complex reordering rules and how to implement them in order to understand the memory visibility that JMM provides.
(chapter III) Java memory model