Multi-core concurrent cache architecture
Cache consistency issues
Cache-based storage system interaction is a good solution to the processor/memory speed conflict, but it also introduces higher complexity to computer systems because of a new problem: cache consistency.
In a multi-processor system (or a single-processor, multi-core system), each processor (each core) has its own cache, and they share the same Main Memory.
When the computation tasks of multiple processors all involve the same main memory area, the cache data of each processor may be inconsistent.
Therefore, each processor must follow some protocols when accessing the cache, and operate according to the protocols when reading and writing the cache to maintain the consistency of the cache.
Java memory model
Components of the Java memory model
Main memory
The Java Memory model specifies that all variables are stored in Main Memory (which is the same name as the Main Memory used to describe physical hardware, but which is only a portion of virtual machine Memory).
The working memory
Each thread has its own Working Memory (also known as local Memory, analogous to the processor cache introduced earlier), which holds a copy of the shared variables in the main Memory used by the thread.
Working memory is an abstraction of the JMM and does not really exist. It covers caches, write buffers, registers, and other hardware and compiler optimizations. The Java memory model abstraction is shown below:
3. Concurrency problems with JVM memory operations
The JVM memory manipulation problem can be summarized by analogy with the physical machine processor processing problem described above. The Java memory model execution processing described below will focus on solving these two problems.
Working memory data consistency
Each thread will save a copy of the shared variable in the main memory when it manipulates data. If multiple threads’ computing tasks involve the same shared variable, their copies of the shared variable will be inconsistent. If this happens, whose copy of the data will be used to synchronize the data back to the main memory?
The Java memory model ensures data consistency through a series of data synchronization protocols and rules, which will be described later.
Instruction reorder optimization
In Java, reordering is usually a way for the compiler or runtime environment to reorder the execution of instructions to optimize program performance.
There are two types of reordering: compile-time reordering and run-time reordering, corresponding to compile-time and run-time environments respectively.
Similarly, instruction reordering is not arbitrary, it needs to satisfy the following two conditions:
- You cannot change the results of a program run in a single-threaded environment. The just-in-time compiler (and processor) needs to ensure that the program complies with the AS-IF-serial attribute. In layman’s terms, in a single-threaded case, you want to give the program the illusion of sequential execution. That is, the result of the reordered execution must be the same as the result of the sequential execution.
- Data dependencies cannot be reordered.
In a multi-threaded environment, if there is a dependency between thread processing logic, the result may be different from the expected result due to instruction reordering. How to solve this situation will be discussed in the Java memory model.
Third, Java memory interaction
Before we look at the protocols and special rules of the Java memory model, let’s first understand the interoperations between memory in Java.
1. Interactive operation process
To better understand memory interactions, let’s take thread communication as an example and see how values are synchronized between threads:
Thread 1 and thread 2 each have a copy of the shared variable x in main memory, which initially has a value of 0.
Updating x to 1 in thread 1 and then synchronizing to thread 2 involves two main steps:
- Thread 1 flushes the updated value of x from thread working memory to main memory.
- Thread 2 goes into main memory and reads the x variable that was updated before thread 1.
As a whole, these two steps are thread 1 sending a message to thread 2, which must pass through main memory.
The JMM provides visibility of shared variables for individual threads by controlling the interaction between main memory and each thread’s local memory.
2. Basic operations of memory interaction
The Java memory model defines the following eight operations to accomplish the specific protocol of interaction between main memory and working memory, namely the implementation details of how a variable is copied from main memory to working memory and synchronized from working memory back to main memory.
Virtual machine implementations must ensure that each of the operations described below is atomic and non-divisible (load, store, read, and write are allowed for exceptions on some platforms for double and long variables).
8 basic operations, as shown below:
- Lock, a variable that acts on main memory and identifies a variable as a thread-exclusive state.
- Unlock, a variable that acts on main memory. It frees a variable that is locked so that it can be locked by another thread.
- Read, a variable acting on main memory that transfers the value of a variable from main memory to the thread’s working memory for subsequent load action.
- Load, a variable that acts on working memory. It puts the value of the variable from main memory into a copy of the variable in working memory.
- Use, a working memory variable that passes the value of a variable in the working memory to the execution engine whenever the virtual machine reaches a bytecode instruction that requires the value of the variable to be used.
- Assign, a working memory variable that assigns a value received from the execution engine to the working memory variable. This operation is performed whenever the virtual machine accesses a bytecode instruction that assigns a value to the variable.
- Store, a variable applied to working memory that passes the value of a variable in working memory to main memory for subsequent write operations.
- Write, a variable that operates on main memory. It puts the value of a variable in main memory that the Store operation fetched from working memory.
Java memory model running rules
1. Three features of the basic operation of memory interaction
Before introducing the eight basic operations of memory interaction, it is necessary to introduce three features of the operation.
The Java memory model is built around how these three features are handled in concurrent processes, and the definitions and basic implementation are briefly introduced here, followed by a step-by-step analysis.
Atomicity (Atomicity)
Atomicity, that is, one or more operations are either all performed without interruption by any factor or none at all.
Even when multiple threads are working together, once an operation has started, it cannot be disturbed by other threads.
Visibility (Visibility)
Visibility means that when multiple threads access the same variable and one thread changes the value of the variable, other threads can immediately see the changed value.
As illustrated in “Interactive Flow” above, the JMM achieves visibility by relying on main memory as the delivery medium by synchronizing the new value back to main memory after the working memory of the variable is modified in thread 1, and flushing the value from main memory before the variable is read in thread 2.
The differentiation (Ordering)
Orderliness rules occur in the following two scenarios:
- Within a thread, from the point of view of a method, instructions are executed in a way called as-if-serial, which is already used in sequential programming languages.
- Between threads, when that thread “watches” another thread execute asynchronously, any code may cross because of instruction reordering optimization. The only constraint that matters is that for synchronized methods, operations on synchronized blocks (the synchronized keyword modifier) and volatile fields remain relatively orderly.
The Java memory model’s set of running rules may seem cumbersome, but in summary, they are built around atomicity, visibility, and orderliness.
Ultimately, the program can run as expected in an environment optimized for achieving data consistency in multiple threads of working memory with shared variables, multi-threaded concurrency, and instruction reordering.