Java Memory Model, referred to as JMM, is a unified guarantee for a series of Java virtual machine platforms to the unrelated specific platform for the memory visibility and whether it can be reordered in a multi-threaded environment provided by developers. (There may be ambiguous in terms of the term and the memory distribution of Java runtime, which refers to memory areas such as heap, method area, thread stack, etc.).
There are many styles of concurrent programming. In addition to CSP (communication sequential process), Actor and other models, the most familiar one should be the shared memory model based on threads and locks. In multi-threaded programming, three types of concurrency problems need to be paid attention to:
・Atomicity・Visibility・Reorder
Atomicity involves whether other threads can see the intermediate state or interfere when a thread performs a composite operation. Typically, it is the problem of i++. Two threads perform ++ operations on the shared heap memory at the same time. The implementation of ++ operations in JVM, runtime, and CPU may be a composite operation. For example, from the perspective of JVM instructions, it is to read the value of i from the heap memory to the operand stack, add one, and write back to the heap memory i. During these operations, if there is no correct synchronization, other threads can also execute it at the same time, which may lead to data loss and other problems. Common atomicity problems, also known as the competitive condition, are judged based on a possible failure result, such as read-modify-write. Visibility and reordering problems both stem from system optimization.
Since the execution speed of the CPU and the access speed of the memory are seriously mismatched, in order to optimize performance, based on localization principles such as time locality and spatial locality, the CPU has added a multi-layer cache between the memory. When it is necessary to fetch data, the CPU will first go to the cache to find out whether the corresponding cache exists. If it exists, it will be returned directly. If it does not exist, it will be fetched into the memory and saved in the cache. Now the more multi-core processors have become standard, each processor has its own cache, which involves the issue of cache consistency. CPUs have consistency models of different strengths and weaknesses. The strongest consistency is the highest security, and it also conforms to our sequential thinking mode. However, in terms of performance, there will be a lot of overhead because of the need for coordinated communication between different CPUs.
A typical CPU cache structure diagram is as follows
The instruction cycle of the CPU is usually instruction fetching, parsing instructions to read data, executing instructions, and writing data back to registers or memory. When executing instructions in serial, the read and stored data take up a long time, so the CPU generally uses the instruction pipeline to execute multiple instructions at the same time to improve the overall throughput, just like a factory pipeline.
The speed of reading data and writing back data to memory is not on the same order of magnitude than executing instructions, so the CPU uses registers and caches as caches and buffers. When reading data from memory, it will read a cache line (similar to disk reading and read a block). The module that writes data back will put the storage request into a store buffer when the old data is not in the cache and continues to execute the next stage of the instruction cycle. If it exists in the cache, the cache will be updated, and the data in the cache will flush to memory according to a certain policy.
public class MemoryModel { private int count; private boolean stop; public void initCountAndStop() { count = 1; stop = false; } public void doLoop() { while(!stop) { count++; } } public void printResult() { System.out.println(count); System.out.println(stop); }}When executing the above code, we may think that count = 1 will be executed before stop = false. This is correct in the ideal state shown in the above CPU execution diagram, but it is incorrect when considering the register and cache buffering. For example, stop itself is in the cache but the count is not there, then the stop may be updated and the count write buffer is refreshed to memory before writing back.
In addition, the CPU and compiler (usually refer to JIT for Java) may modify the instruction execution order. For example, in the above code, count = 1 and stop = false have no dependencies, so the CPU and compiler may modify the order of these two. In the view of a single-threaded program, the result is the same. This is also the as-if-serial that the CPU and compiler must ensure (regardless of how the execution order is modified, the execution result of the single-threaded remains unchanged). Since most of the program execution is single-threaded, such optimization is acceptable and brings great performance improvements. However, in the case of multithreading, unexpected results may occur without the necessary synchronization operations. For example, after thread T1 executes the initCountAndStop method, thread T2 executes printResult, which may be 0, false, 1, false, or 0, true. If thread T1 executes doLoop() first and thread T2 executes initCountAndStop one second, then T1 may jump out of the loop, or it may never see the modification of stop due to compiler optimization.
Due to the various problems in the above multi-threading situations, the program sequence in multi-threading is no longer the execution order and result in the underlying mechanism. The programming language needs to give developers a guarantee. In simple terms, this guarantee is when a thread's modification will be visible to other threads. Therefore, the Java language proposes JavaMemoryModel, that is, the Java memory model, which requires implementation in accordance with the conventions of this model. Java provides mechanisms such as Volatile, synchronized, and final to help developers ensure the correctness of multi-threaded programs on all processor platforms.
Before JDK1.5, Java's memory model had serious problems. For example, in the old memory model, a thread might see the default value of a final field after the constructor is completed, and the write of the volatile field may be reordered with the read and write of the non-volatile field.
So in JDK1.5, a new memory model was proposed through JSR133 to fix the previous problems.
Reorder rules
volatile and monitor lock
| Is it possible to reorder | The second operation | The second operation | The second operation |
|---|---|---|---|
| The first operation | Normal reading/ordinary writing | volatile read/monitor enter | volatile write/monitor exit |
| Normal reading/ordinary writing | No | ||
| voaltile read/monitor enter | No | No | No |
| volatile write/monitor exit | No | No |
The normal read refers to the arrayload of getfield, getstatic, and non-volatile arrays, and the normal read refers to the arraystore of putfield, putstatic, and non-volatile arrays.
The read and write of volatile fields are getfield, getstatic, putfield, putstatic, respectively.
monitorenter is to enter the synchronization block or synchronization method, monitorexist refers to exiting the synchronization block or synchronization method.
No in the above table refers to two operations that do not allow reordering. For example (normal writing, volatile writing) refers to the reordering of non-volatile fields and the reordering of writes of any subsequent volatile fields. When there is no No, it means that reordering is allowed, but the JVM needs to ensure minimum security - the read value is either the default value or written by other threads (64-bit double and long read and write operations are a special case. When there is no volatile modification, it is not guaranteed that read and write are atomic, and the underlying layer may split it into two separate operations).
Final field
There are two additional special rules for the final field
Neither the write of the final field (in the constructor) nor the write of the reference of the final field object itself can be reordered with subsequent writes of the objects holding the final field (outside the constructor). For example, the following statement cannot be reordered
x.finalField = v; ...; sharedRef = x;
The first load of the final field cannot be reordered with the write of the object holding the final field. For example, the following statement does not allow reordering.
x = sharedRef; ...; i = x.finalField
Memory barrier
Processors all support certain memory barriers or fences to control the visibility of reordering and data between different processors. For example, when the CPU writes data back, it will put the store request into the write buffer and wait for flushing into memory. This store request can be prevented from being reordered with other requests by inserting the barrier to ensure the visibility of the data. You can use a life example to compare the barrier. For example, when taking a slope elevator on the subway, everyone enters the elevator in sequence, but some people will go around from the left, so the order when leaving the elevator is different. If a person carries a large luggage blocked (barrier), the people behind cannot go around :). In addition, the barrier here and the write barrier used in GC are different concepts.
Classification of memory barriers
Almost all processors support barrier instructions of a certain coarse grain, usually called Fence (fence, fence), which can ensure that the load and store instructions initiated before fence can be strictly in order with the load and store after fence. Usually, it will be divided into the following four types of barriers according to their purpose.
LoadLoad Barriers
Load1; LoadLoad; Load2;
Ensure that Load1 data is loaded before Load2 and after load
StoreStore Barriers
Store1; StoreStore; Store2
Ensure that the data in Store1 is visible to other processors before Store2 and after.
LoadStore Barriers
Load1; LoadStore; Store2
Ensure that the data of Load1 is loaded before Store2 and after data flush
StoreLoad Barriers
Store1; StoreLoad; Load2
Ensure that the data in Store1 is visible in front of other processors (such as flushing to memory) before loading the data in Load2 and after load. StoreLoad Barrier prevents load from reading old data rather than data recently written by other processors.
Almost all multiprocessors in modern times require StoreLoad. The overhead of StoreLoad is usually the largest, and StoreLoad has the effect of three other barriers, so StoreLoad can be used as a general (but higher overhead) barrier.
Therefore, using the above memory barrier, the reordering rules in the above table can be implemented
| Need barriers | The second operation | The second operation | The second operation | The second operation |
|---|---|---|---|---|
| The first operation | Normal reading | Normal writing | volatile read/monitor enter | volatile write/monitor exit |
| Normal reading | LoadStore | |||
| Normal reading | StoreStore | |||
| voaltile read/monitor enter | LoadLoad | LoadStore | LoadLoad | LoadStore |
| volatile write/monitor exit | StoreLoad | StoreStore |
In order to support the rules of final fields, it is necessary to add a barrier to the final write to final
x.finalField = v; StoreStore; sharedRef = x;
Insert memory barrier
Based on the above rules, you can add a barrier to the processing of volatile fields and synchronized keywords to meet the rules of the memory model.
Insert the StoreStore before the volatile store barrier after all final fields are written but insert the StoreStore before the constructor returns
Insert the StoreLoad barrier after volatile store. Insert the LoadLoad and LoadStore barrier after volatile load.
The monitor enter and volatile load rules are consistent, and the monitor exit and volatile store rules are consistent.
HappenBefore
The various memory barriers mentioned above are still relatively complex for developers, so JMM can use a series of rules of partial order relationships of HappenBefore to illustrate. To ensure that the thread that executes operation B sees the result of operation A (regardless of whether A and B are executed in the same thread), then the HappenBefore relationship must be met between A and B, otherwise the JVM can reorder them arbitrarily.
HappenBefore Rule List
HappendBefore rules include
Program sequence rules: If operation A in the program is before operation B, then operation A in the same thread will perform monitor lock rules before operation B: The lock operation on the monitor lock must be performed before the lock operation on the same monitor lock.
volatile variable rules: The write operation of the volatile variable must execute thread startup rules before the read operation of the variable: The call to Thread.start on the thread must execute thread end rules before any operation in the thread: Any operation in the thread must execute interrupt rules before other threads detect that the thread has ended: When a thread calls interrupt on another thread, it must execute passivity before the interrupted thread detects interrupt: If operation A is executed before operation B and operation B is executed before operation C, then operation A is executed before operation C.
The display lock has the same memory semantics as the monitor lock, and the atomic variable has the same memory semantics as the volatile. The acquisition and release of locks, the read and write operations of volatile variables satisfy the full-order relationship, so the write of volatile can be performed before subsequent volatile reads.
The above-mentioned HappenBefore can be combined using multiple rules.
For example, after thread A enters the monitor lock, the operation before releasing the monitor lock is based on the program sequence rules, and the monitor release operation HappenBefore is used to obtain the same monitor lock in the subsequent thread B, and the operation in the operation in HappenBefore and thread B.
Summarize
The above is all the detailed explanation of Java memory model JMM in this article, I hope it will be helpful to everyone. If there are any shortcomings, please leave a message to point it out. Thank you friends for your support for this site!