- 1, the computer hardware memory structure
- Background and definition of the Java memory model
- Java memory model
- 3.1 Definitions of main memory and working memory
- 3.2 Memory Interaction
- 3.3 JMM Cache Inconsistency
- 4. Implementation of Java memory model
1, the computer hardware memory structure
In a single-core computer, the CPU in the computer is very fast, but with other hardware in the computer (such as IO, memory, etc.) with the CPU speed is far from the difference, so coordination between the CPU and each hardware speed difference is very important, otherwise the CPU has been waiting, wasting resources. While this is true with a single core, the problem is even more pronounced with multiple cores. The hardware structure is shown in the figure below:
Let’s take a look at the process: When our computer to perform a task or calculate a number, main memory will be the first computer calculations required data to be loaded from the database, because of the memory and CPU speed difference is bigger, so it is necessary to introduce between memory and CPU cache (according to the actual need, can introduce multilayer cache, main memory data will be stored in the CPU of the cache, When the data is needed to interact with the CPU, it is added to the CPU register and finally used by the CPU.
In fact, in the case of a single core, based on the interaction of cache can well solve the speed matching between CPU and other hardware, but in the case of multi-core, each processor must follow certain agreements to ensure each processor in the memory cache and the problem of data consistency in main memory, such agreements are often referred to as the cache consistency protocol.
Background and definition of the Java memory model
We in the development will often meet with such a scenario, we developed the code on the operation environment of our own performance is good, but when we put it on other hardware platform, can appear all sorts of mistakes, this is because the different hardware manufacturers and under different operating systems, memory access logic has certain difference, The result is that when you have code that works well on one system and is thread-safe, you can switch systems and have all sorts of problems.
To solve this problem, the Concept of the Java Memory Model (JMM) was proposed. It can mask the differences between system and hardware, so that a set of code on different platforms can reach the same access results, achieve platform consistency, so that Java programs can be written once, run anywhere.
This is a familiar description of the JVM. What is the difference between the two?
What are the differences between JVM and JMM?
In effect, JMM is how the Java Virtual Machine (JVM) works in computer memory (RAM), and JMM defines an abstract relationship between threads and main memory: Shared variables between threads are stored in Main Memory. Each thread has a private Local Memory where it stores a copy of the shared variable to read/write. Local Memory is a JMM abstraction and does not really exist. It covers caching, write buffers, registers, and other hardware and compiler optimizations. The JVM describes the relationships within and between Java virtual machines.
Since JMM defines the relationship between threads and main memory, does it solve the problem of concurrency? That’s right, let’s review the key issues in the concurrency space.
Key issues in concurrency?
- Communication between threads
In programming, there are two communication mechanisms between threads, shared memory and message passing.
In the shared memory concurrency model, threads share the common state of the program and communicate with each other implicitly through the common state of the writer-read memory. The typical shared memory communication mode is to communicate through shared objects.
In the concurrent model of messaging, there is no common state between threads and threads must communicate explicitly by explicitly sending messages. Typical messaging methods in Java are wait() and notify().
- Synchronization between threads
Synchronization is the mechanism that a program uses to control the relative order in which operations occur between different threads. In the shared memory concurrency model, synchronization is done explicitly. Programmers must explicitly specify that a method or piece of code needs to be executed mutually exclusive between threads. In the concurrent model of message delivery, synchronization is implicit because the message must be sent before the message is received.
In fact, the Java Memory Model (JMM) uses the shared memory model for concurrency.
Let’s take a look at the Java memory model
Java memory model
Let’s start with a diagram of the JMM control model
It can be seen that the Java Memory model (JMM) is similar to the CPU cache model in structure and is based on the CPU cache model.
We first comb the working process of the JMM pictured above example, we assume that A quad-core computer, cpu1 thread A operation, cpu2 operation thread B, cpu3 thread C operation, when the three thread needs to be operated on the Shared variables in main memory, the three threads respectively in main memory will be Shared memory read in their working memory, Keep a copy of the shared variable for your own thread to use.
At this time some partners may have the following questions:
-
What are the definitions of main memory and working memory?
-
How do I read a shared variable from main memory into my own thread’s own working memory?
-
When one of the threads modifies a shared variable, does the value of the shared variable change in the rest of the threads, and if so, how does it remain visible between threads?
Below, we answer these two questions one by one.
3.1 Definitions of main memory and working memory
- Main memory
Main memory stores Java instance objects, that is, all instance objects created by threads are stored in main memory, whether they are member variables or local variables (also known as local variables) in methods, as well as shared class information, constants, and static variables. Since it is a shared data area, multiple threads accessing the same variable may find thread-safety issues.
- The working memory
Working memory stores information about all local variables of the current method (working memory stores copies of variables in main memory), that is, each thread can only access its own working memory, that is, local variables in a thread are not visible to other threads, even if two threads are executing the same code. They also create local variables in their working memory that belong to the current thread, including bytecode line number indicators and information about Native methods. Note that since working memory is the private data of each thread, threads cannot access working memory with each other, so data stored in working memory is not thread-safe.
**NOTE:** The main memory, working memory and the Java heap, stack, and method areas in the Java memory area are not the same level of memory partition, the two are basically unrelated.
Once you understand the main memory and working memory, the next step is to learn how the main memory interacts with the working memory data.
3.2 Memory Interaction
There are eight types of interaction between main memory and working memory, each of which a VIRTUAL machine must ensure is atomic:
- The Lock (Lock)
A variable acting on main memory that identifies a variable as a thread-exclusive state.
- Unlock (unlocked)
A variable applied to main memory that frees a locked variable before it can be locked by another thread
- Read (read)
Function on a main memory variable, which transfers the value of a variable from main memory to the thread’s working memory for subsequent load action
- Load (load)
A variable operating on working memory that puts a read operation from a variable in main memory into working memory
- Use (used)
Function on a variable in working memory, which transfers the variable in working memory to the execution engine and is used whenever the virtual machine reaches a value that requires the variable
- The assign (assignment)
Function on a variable in working memory, which puts a value received from the execution engine into a copy of the variable in working memory
- Store (storage)
Function on a variable in main memory, which passes a value from a variable in working memory to main memory for subsequent write use
- Write (to write)
Applied to a variable in main memory, which puts the value of a variable in main memory that the store operation fetched from working memory
Looking at these eight types of atomic operations may seem a little abstract, but let’s draw a flow chart to sort it out.
Operation flow chart:
As you can see, if you want to copy a variable from memory to working memory, you need to perform read and load operations sequentially, and if you want to synchronize a variable from working memory to main memory, you need to perform store and write operations.
NOTE: The Java memory model only requires that these operations be performed sequentially, not sequentially.
Let’s take two threads as examples to comb out the operation process:
Suppose there are two threads A and B. If thread A wants to communicate with thread B, thread A first flusher the updated shared variable from local memory A to main memory. Thread B then goes into main memory to read the shared variables that thread A has updated previously.
If multiple threads read and modify the same shared variable at the same time, this situation may cause the same cache variable in the local memory of each thread. How to solve this problem?
3.3 JMM Cache Inconsistency
There are two solutions to the problem of local memory variable cache inconsistency in the JMM, namely bus locking and MESI cache consistency protocol.
The bus lock
When a CPU reads data from main memory to local memory, it locks the data on the bus so that no other CPU can read or write the data until the CPU uses up the data and releases the lock.
Bus locking can ensure data consistency, but it seriously reduces system performance, because when a thread multi-bus locking, other threads can only wait, the original parallel operation into serial operation.
Typically, we don’t take this approach and use a high-performance cache consistency protocol instead.
MESI Cache consistency protocol
The MESI cache consistency protocol allows multiple cpus to read the same data from main memory into their respective caches. When one of the cpus modifies the data in the cache, the data is immediately synchronized back to main memory. The other cpus can sense the change through the bus sniffing mechanism and invalidate their own caches.
In concurrent programming, if multiple threads are operating on the same shared variable, we typically use volatile to prefix the variable name, because it ensures visibility of changes to the variable based on the fact that multiple threads are listening on the bus. When one thread modifies a shared variable, the variable is immediately synchronized to main memory, and when other threads hear the change, they invalidate their cache and trigger the read operation to read the value of the newly modified variable. Thus, data consistency of multiple threads is guaranteed. In fact, volatile relies on the MESI cache consistency protocol for how it works.
4. Implementation of Java memory model
In Java multithreading, Java provides a number of keywords related to concurrent processing, such as volatile, synchronized, final, concurren packages, and so on. These are the keywords that the Java memory model provides to programmers by encapsulating the underlying implementation
In fact, the essence of the Java memory model is designed around how Java concurrency handles atomicity, visibility, and sequentiality, which can be implemented directly using keywords provided in Java and are frequently asked in interviews.
- atomic
Atomicity is defined as an operation that cannot be interrupted, either completely or not at all. It is somewhat similar to a transaction in that it either succeeds or falls back to where it was before the operation.
The atomic variable operations guaranteed by the JMM include read, Load, assign, use, Store, and write
NOTE: Access to basic data is mostly atomic. Long and double variables are 64-bit, but in a 32-bit JVM, the 32-bit JVM will divide the 64-bit data into two 32-bit reads and writes. This results in non-atomic manipulation of long and double variables in a 32-bit virtual machine, which can corrupt data, meaning that multiple threads accessing them concurrently are not thread-safe.
For basic types of non-atomic operations, synchronized can be used to ensure that operations within methods and code blocks are atomic.
1 synchronized (this) {
2 a=1;
3 b=2;
4 }
Copy the code
If a thread observes another thread executing the code above, it will only see the result that both a and B have been assigned successfully, or that both a and B have not been assigned yet.
- visibility
The Java memory model relies on main memory as a transfer medium by synchronizing the new value back to main memory after a variable is modified and flushing the value from main memory before the variable is read.
The volatile keyword in Java provides the ability to synchronize modified variables to main memory immediately after they are modified, and to flush variables from main memory each time they are used. Therefore, volatile can be used to ensure visibility of variables in multithreaded operations.
In addition to volatile, the Java keywords synchronized and final are also visible. It’s just implemented in a different way. It’s not expanded anymore.
- order
In Java, synchronized and volatile can be used to ensure order between multiple threads. Implementation methods are different:
The volatile keyword disallows instruction reordering. The synchronized keyword ensures that only one thread can operate at a time.
Ok, so this is a brief introduction to the keywords that can be used to solve atomicity, visibility, and order in Java concurrent programming. As readers may have noticed, synchronized seems to be a one-size-fits-all keyword that satisfies all three of these attributes, which is why so many people abuse synchronized.
Synchronized, however, is a performance deterrent, and while the compiler provides many lock optimization techniques, overuse is not recommended.
reference
[1]https://www.jianshu.com/p/8a58d8335270 [2]https://blog.csdn.net/javazejian/article/details/72772461 [3]https://blog.csdn.net/zjcjava/article/details/78406330 [4]https://segmentfault.com/a/1190000016085105