In this second article on the JVM, we look at a concept that is easily confused with JVM memory structures: the JVM memory model. If you are not familiar with the memory structure, you can first visit: Roaming JVM(a) : JVM memory structure
The Java Memory Model is a mechanism for communication between threads. It describes the relationship between main Memory and thread local Memory in the JVM. In the design of Java virtual machine, the interaction between [Memory] and [CPU cache] in the computer is referred to. Therefore, the memory model of both JVMS is very similar to that of computers.
1 Memory model for the JVM
JMM abstracts the concepts of main memory and thread-local memory. From an abstract point of view, the JMM defines an abstract relationship between threads and main memory: shared variables between threads are stored in main memory, and each thread has a private thread-local memory that stores a copy of the shared variables to read/write.
2 Three Major Properties
When describing and analyzing Java concurrent programming, we often refer to atomicity, visibility, and orderliness as starting points, and we can also use these as starting points when we study the JMM.
2.1 atomic
Atomicity refers to the fact that an operation is uninterruptible and either all of it succeeds or all of it fails. In JMM, the interaction between main memory and thread-local memory is achieved through the following eight atomic operations:
- Read: Acts on a main memory variable that transfers the value of a variable from main memory to the thread’s working memory for subsequent load action
- Load: Variable acting on working memory, which puts a read operation variable from main memory into working memory
- Use: Applies to variables in working memory. It transfers variables in working memory to the execution engine. This instruction is used whenever the virtual machine reaches a value that requires the variable to be used
- Assign: A variable applied to working memory that places a value received from the execution engine into a copy of the variable in working memory
- Store: Acts on a variable in main memory. It transfers a value from a variable in working memory to main memory for subsequent writes
- Write: A variable in main memory that puts the value of a variable in main memory from the store operation in working memory
- Lock: A variable that acts on main memory, marking a variable as thread-exclusive
- Unlock: A variable that acts on main memory. It releases a locked variable so that it can be locked by another thread
2.2 the visibility
Visibility means that when one thread changes a variable in working memory, other threads can immediately know about the change.
For the convenience of description, variable A in main memory is called [main A] below, and the backup of the respective threads is [line A1] and [line A2].
Two threads are not visible:1. The data of [Principal A] is10
2Threads.1And thread2Access main memory to obtain A backup of [line A1] and [line A2].3Threads.1Modify line A1, for example, to15
4.thread at this time2Line A2 in is still10
Copy the code
How to solve this problem?
MESI (Cache Consistency Protocol)
In Java, this visibility problem can be solved by using the volatile keyword to modify variable A. MESI requires that when a thread changes a variable in local memory, it must immediately write the variable to main memory, while other threads mark their copy of local memory as invalid, and then load the latest data back into main memory when using it again.
In the JMM context, the volatile keyword is used as volatile:
- After modifying the volatile keyword, turn on MESI
- When a thread assigns a variable in local memory to [assign], it immediately writes the data to main memory by [store]->[write]
- [loca]->[store]->[write]->[unlock]. If a thread wants to acquire data again, it can only read the data after the write is complete and the lock is released.
- Other threads are constantly listening to the bus because MESI is enabled, and when they hear that data is about to be written (to the bus), they mark their own copy of local memory as invalid. If they still need the data, they have to go back to main memory to read the latest copy.
This is how volatile solves the visibility problem. In addition to addressing visibility, volatile also solves the ordering problem.
2.3 order
In the Java memory model, the compiler and processor are allowed to reorder instructions, but the reordering process does not affect the execution of a single-threaded program, but affects the correctness of multithreaded concurrent execution.
Why do we need to reorder? The main purpose is to reduce the register reading and storage times as much as possible without changing the semantics of the program, and fully reuse the stored value of the register. Such as:
1.int a = 1; 2.int b = 2; 3.int c = a+1;
Copy the code
- Assuming the compiler does not reorder, ① needs to interact with the register once it is done (put in) and ③ needs to interact with the register once it is done (take out).
- If the compiler uses reorder, the order of execution would be ①, ③, and ②. After ① is done, the order of execution would be ①, ③, and ②.
Such reordering is not a problem in a single-threaded environment because the end result is correct, but in a multi-threaded environment, such reordering can lead to thread insecurity. If another thread needs to read A and a is not stored in main memory due to reordering, the data read by this thread is not the latest value.
In Java, the volatile keyword is used to tell the compiler not to reorder the variable.
Happens-before principle
The Java memory model has some innate “orderliness,” that is, orderliness that can be guaranteed without any means, which is often referred to as the happens-before principle. If the order of two operations cannot be deduced from the happens-before principle, they are not guaranteed to be ordered, and the virtual machine can reorder them at will.
1. Rules of sequence of procedures:
The execution result of a piece of code in a thread is ordered. It will rearrange the instructions, but whatever it does, the results will be generated in the same order as our code.
2. Pipe lock rules:
Whether in single-threaded or multi-threaded environment, for the same lock, after one thread to unlock the lock, another thread can see the operation result of the previous thread! (A pipe is a generic synchronization primitive, and synchronized is its implementation)
3. Volatile variable rules:
If a thread writes a volatile variable and then reads it, the result of the write must be visible to the thread reading it.
4. Thread start rule:
During the execution of the main thread A, child thread B is started, so the modification of shared variables made by thread A before starting child thread B is visible to thread B.
5. Thread termination rules:
During the execution of the main thread A, the child thread B terminates, so the modification of the shared variable made by thread B before termination is visible in thread A. Also called the thread Join () rule.
6. Thread interrupt rule:
A call to the thread.interrupted () method occurs when the interrupted Thread code detects the occurrence of an interrupt event, which can be detected by thread.interrupted ().
7. Transitivity Rules:
The simple one is that the happens-before principle is transitive, hb(A, B), HB (B, C), so HB (A, C).
8. Object termination rule:
The completion of an object’s initialization, that is, the end of the constructor execution must happens-before its finalize() method.
3 summary
The main content of this section is the memory model around the JVM:
- The memory model of the JVM
- Main memory
- Thread worker thread
- The three properties
- atomic
- Eight atomic operations
- visibility
- MESI (Cache Consistency Protocol)
- order
- Happens-before principle
- atomic