JMM (java-memory-model, java memory model) specification

From "In-depth understanding of the JAVA memory model" - Cheng Xiaoming plus personal understanding, because some knowledge points have been checked a lot and still not sure what it means, there is a personal understanding in it, for reference only

1. What is JMM

JAVA's concurrent programming model, shielding the hardware layer, is a high-level abstraction that divides the memory area into main memory and local memory (logical division, not corresponding to a real area), shields the hardware layer, and is possible for multi-threaded programming. The resulting orderliness, memory visibility issues, and the proposed solution model.

JMM abstracts memory into main memory and local memory, as shown in the figure:

Notice:

  • The variables shared between threads here mainly refer to the heap and metaspace. The information in the stack will not be shared between threads, there will be no memory visibility problem, and it will not be affected by JMM.
  • The distinction between main memory and local memory here is mainly due to the difference between caches, write buffers, registers, and other hardware and compiler optimizations, resulting in differences between thread local memory and main memory.

2. Preliminary knowledge

data dependency

Two operations access a variable, one of which is a write, and the two operations have data dependencies, such as read after write, write after write, and write after read.

control dependencies

There is a control dependency between A and B operations, which means that whether the B operation is executed depends on the result of A. The compiler and processor may rearrange the A operation and the B operation by guessing execution, such as executing the B operation in advance to obtain the B operation. The result of the operation is stored in a temporary variable. When A has a result, this temporary variable is assigned to the variable that should be assigned.

In a single thread, this is no problem, but in a multi-thread, the execution result of the program may be changed.

reordering

There are several reasons for reordering in java:

  • JIT compilation reordering JIT compilation will optimize the instruction execution order based on efficiency. JIT refers to Java just-in-time compilation, which performs deep machine code compilation for hot code, which can speed up code execution, because the speed of one visit and one interpretation execution is not as fast as the speed of directly executing compiled machine code
  • Instruction reordering CPU execution instructions may not be executed in order, and the execution order will be optimized based on efficiency.
  • Memory reordering Due to the existence of hardware write buffers and invalid queues, the following code is executed before writing to main memory is completed. Other CPUs cannot perceive the execution of previous instructions, which is also a disorder. (Some people say that there is a cache coherence protocol, why does memory reordering occur, the reason is that MESI does not completely solve the problem, because the write buffer and invalid queue are optimized on the hardware, which can only use the assembly of memory barriers instructions are controlled at the coding level).

The latter two types of instruction reordering and memory reordering can be combined as CPU reordering .

Note: Any reordering will take into account data dependencies and will not change the execution order of two operations that have data dependencies. It only refers to the data dependencies of operations executed in a single thread or a single processor, and between different processors or different threads. The logical data dependencies that arise between them are not taken into account by the compiler and processor.

Result of reordering: It may cause memory visibility
problems in multithreaded programs . Although the above reordering will not cause problems under single thread/single processor, it does not consider that in the case of multithreading, if two threads have data dependencies , you should use a synchronization mechanism to avoid it.

3. Core concepts of JMM

happens-before principle

The concept proposed by JSR-133 (JMM Specification Proposal) is used to describe the memory visibility between operations, providing a simple and easy-to-understand rule for programmers to judge the memory visibility between two operations, so that the program can Operators don't need to care about the reasons behind memory visibility, which may involve complex reordering rules and the specific implementation of those rules. content include:

  • Program order rules: each operation in a thread, happen-before and any subsequent operations in the thread (personal understanding is the program order written in the code).
  • The volatile variable rules, for the write operation of the volatile modified variable, must happen-before the subsequent read operation of the volatile variable;
  • Monitor lock rules: unlocking a monitor, happens-before then locking the monitor (monitor lock, refers to synchronized)
  • Transitive: If A happens-before B, and B happens-before C, then A happens-before C.
happen-before meaning:

Operation A happens-before operation B means that the result of operation A is visible to operation B. The meaning is just that the result of operation A is visible to operation B.
It is not required that A must be executed before B. When B does not need the execution result of A to be visible (there is no data dependency), A and B can still be reordered, and such reordering is not illegal.

as-if-serial semantics

Is a semantic, compiler, runtime, processor will obey. It means that no matter how reordered, the execution result of the program cannot be changed. Personally understand that the order of operations that generate data dependencies cannot be changed, but the order of operations without data dependencies may change.

Sequentially consistent memory model

It is a theoretical model. JMM is not a sequential consistency model, which means that all operations of a thread must be executed in the order of the program , and all threads can only see a single operation execution order. In a sequentially consistent memory model, every operation must execute atomically and be visible to all threads at once.
The difference between JMM and it:

  • Single-threaded operations are not guaranteed to be executed in program order
  • All threads are not guaranteed to see a consistent order of execution
  • Atomicity of writes to 64-bit variables is not guaranteed (32-bit systems)

But the JMM makes a guarantee: if the program is properly synchronized, the execution of the program will be sequentially consistent - that is, the execution of the program will be the same as the execution of the program in the sequential consistency model. However, JMM allows compilers and cpu optimizations such as synchoronized to ensure the atomicity of coding in the critical section without changing the result of the correct synchronized program. Reorder.

Fourth, the implementation method to ensure correct synchronization in JMM

Note: The following is a confusing place for personal understanding, and there is no unified answer on the Internet. It is right here as a record for reference only.

It mainly refers to the synchronization mechanism provided by java, such as volatile and synchronized. They use memory barriers (note that the semantics of memory barriers here are different from those of the hardware layer, and the memory barriers here refer to the memory barriers abstracted by JMM). The volatile keyword is mainly introduced here, and synchronized is reserved for a separate summary later.

The volatile feature is to ensure memory visibility and ensure the atomicity of reading and writing a single variable.
The semantics of the memory level are: reading volatile invalidates the local memory variable and reads it from the main memory. Writing volatile synchronizes local memory shared variables to main memory.

Mainly how to achieve this memory semantics. My understanding is:

When writing volatile:
  1. When v is written, the normal program flow of the current thread has been read and written, so as to ensure that all the main memory that should be written to the main memory is flushed into the main memory. If other threads read v, they can also analyze the current thread according to the sequential consistency model.
  2. Make sure to sync to main memory when the write completes.
When reading volatile:
  1. When v reads, all subsequent reads are not executed under the normal program flow, so as to ensure that the data read after v reads are in the main memory.
  2. At the same time, it should be avoided that the write after v read is reordered before v read, to ensure that when other threads perform v write, the main memory that should not be written has not been written yet.

As mentioned earlier, reordering is divided into compiler reordering and CPU reordering. Let's talk about how JMM solves these two rearrangements.

Compiler rearrangement

Mainly based on the reordering rules as shown below:

Note: The first operation and the second operation do not refer to two adjacent operations, but to two operations before and after a single thread.

As can be seen from the figure:

  • When the second operation is a volatile write, no matter what the first operation was, it cannot be rearranged after the volatile write.
  • When the first operation is a volatile read, no matter what the second operation is, it cannot be reordered before the volatile read.
  • When the first operation is a volatile write and the second operation is a volatile read, reordering is not possible.

Personal understanding:

  • So that when thread communication is formed (volatile write of thread A, volatile read of thread B), the trigger of this timing can analyze thread A according to the sequential consistency memory model, that is, thread B can understand that the instruction before thread A volatile write has been executed. , thread A can understand that the command after thread B's volatile read has not been executed.
  • This ensures that the compiler reordering will not affect the visibility (the arbitrary read and write of the default variables is no different in the local memory of each thread), because the reason for the difference in the local memory of each thread is the cpu reordering, so it is necessary to solve this problem. It is necessary to introduce memory barriers at the level of cpu reordering to solve cpu reordering.
CPU reordering

It mainly introduces a memory barrier. The memory barrier is inserted by java when compiling and generating bytecode, and it is not specific to the hardware level. It belongs to the abstract concept in JMM. Different processors may have slightly different implementations. There are four types. :

  • LoadLoad barrier

Order control semantics: The preceding Load and the following Load cannot be out of order.

  • StoreStore Barrier

Order control semantics: The preceding Store and the following Store cannot be out of order. Flush main memory control semantics: The previous Store will also flush the thread-local memory to the main memory before the latter Store.

  • LoadStore barrier

Order control semantics: The preceding Load and the following Store cannot be out of order. Flush main memory control semantics: Ensure that Load is executed before the next Store flushes main memory.

  • StoreLoad barrier

Order control semantics: The preceding Store and the following Load cannot be out of order. Refresh main memory control semantics: After the Store is completed, the thread local memory will be refreshed to the main memory.

The above is the semantic meaning of personal understanding, and there may be deviations. The concepts in the book are posted below:

In addition, I personally think that the Load and Store before and after should be referred to in multiple threads rather than a single thread. For example, after StoreLoad, Load not only refers to the Load of the thread, but also refers to the Load of other threads. If this is not the case, it feels unexplainable.

memory barrier for volaite read and write inserts

In fact, I didn't fully understand why this part was inserted in this way, and I had some doubts, so I probably wrote it according to the introduction in the book, without giving too much introduction.

volatile writes:

Insert a StoreStore barrier before each volatile write operation, and insert a StoreLoad barrier after the write operation;

Question: Why is the LoadStore barrier not added in front? (The compiler reordering rules clearly state that ordinary reads and volatile writes cannot be reordered)

volatile reads:

Insert LoadLoad barriers after each volatile read operation, and LoadStore barriers;

Note: The above memory barriers are too conservative. Due to differences in operating systems and some unnecessary situations such as barrier repetition, the compiler will optimize and omit the barrier according to the situation.

Finally : I feel that I am still too naive. After reading this concept for about a week, I still don't quite understand it, but I still make a record.

{{o.name}}
{{m.name}}

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324109015&siteId=291194637