This article was first published on the public account: Lao Hu code word

Before introducing the Java memory model, let’s look at the memory model for computer hardware, because concurrency in the JVM is very similar to concurrency in a physical machine, and even much of the design of concurrent JVM operations is caused by the design of the computer system.

Memory model of hardware

We all know that the computer system is mainly processing tasks by the processor (CPU) for operation, the operation will involve in the data, where data, data is stored in computer memory, so a processor would inevitably involve in the process of operation and memory read and write interactions, such as the required data read operation, to store the data results of operation and so on. The processing speed of the processor is much faster than the reading and writing speed of the physical memory, so the processor will wait for the end of the reading and writing of the memory data before carrying out the next operation. Therefore, in order to improve the computing speed of the computer, Modern computer systems add a layer of read and write speeds to the processor’s cache to mitigate the performance difference between memory and the processor. In this way, the data required by the operation is copied to the cache during the processing of the task. After the operation, the data is synchronized from the cache to the memory. In this way, the processor does not need to wait for the end of the memory data reading and writing.

The interaction between processor, cache, and memory is shown as follows:

Pictured above, for each processor in a multiprocessor system has its own cache, so this raises a new problem, if more than one processor computing tasks are involved in the same block of memory area, can lead to their cache data inconsistency, at this time of the data from the cache to write back to main memory will be subject to who? This is where the introduction of caching raises a new issue we call cache consistency.

To address cache consistency, modern computer systems require each processor to follow several protocols (MSI, MESI, MOSI, Synapse, Firefly, DragonProtocal, all of which are caching protocols) for reading and writing cache.

Since we are talking about the memory model of hardware, what is the memory model?

The memory model can be understood as an abstraction of read and write access to a particular memory and cache under a particular operating protocol. Different physical machines may have different “memory models”.

In addition to increasing the cache for the processor, the processor also optimizes the out-of-order execution of the input code programs to ensure that the out-of-order execution results are consistent with the sequential execution results. Here’s an example:

int a = 1;
int b = 2;
int c = a + b;
Copy the code

The above code transposes the first and second lines without any effect on the final result. In order to optimize performance, the processor also performs a similar switch of the code execution order (under the premise of ensuring the same result). This switch of the execution order is called instruction reordering, and similar instruction reordering optimization functions exist in the JVM. As for why instruction reorder optimizes performance and how it optimizes performance, this involves the knowledge of assembly instructions. I also don’t understand assembly instructions, so I won’t introduce them here. If you are interested, you can learn about them yourself.

Java memory model

Different physical machines may have different “memory models”. The memory model defined in the Java VIRTUAL machine can mask different hardware memory models. This ensures that Java programs can achieve consistent memory access across platforms. Because the memory model shields us from the differences between hardware platforms.

Main memory and working memory

The Java memory model specifies that all variables are stored in main memory (the portion of virtual machine memory), which corresponds primarily to Java heap memory. Variables are mentioned here refers to Shared variables, there is competition between threads of variables, such as: instance variables, static variables, and constitute elements of the array object, and local variables and parameters for thread private, so there is no Shared between threads and competitive relationship, so it is beyond the scope of the aforementioned variables.

Each thread has its own working memory, which holds the variables used by the thread. These variables are copied from the main memory variables. All reads and writes to variables by a thread must be done in working memory, not in main memory. The working memory of different threads is also independent; one thread cannot access variables in the working memory of other threads.

When a thread is working, it copies the variables needed from the main memory to its own working memory, and then writes the variables in its working memory back to the main memory after the thread runs. The interaction between multiple threads on variables can only be realized indirectly through the main memory. The interaction diagram of thread, working memory and main memory is as follows:

From the above figure and the previous introduction, it is easy to understand how the problem of inconsistent data state arises when we talk about multi-threaded programming. Such as: Thread 1 and thread 2 requires operating main memory Shared variables in A, when A thread 1 has been modified in the working memory Shared variables. A copy of the value but haven’t written back to the main memory and then thread 2 copies of the Shared variables in the main memory A to their working memory, and then thread 1 will their working memory had Shared variables A copy of the written back to the main memory, It is clear that thread 2 is loading the shared variable A with the previous old state of the data, thus creating the problem of inconsistent data state.

The relationship between Java memory model and hardware memory model

If you look at the interaction diagram of the Java memory model and the hardware memory model, you can see that the two memory models are actually very similar. In fact, the Java program will eventually map to the specific hardware processor kernel during the running process, but the Java memory model and the hardware memory model are not completely consistent.

Only for hardware memory registers, the concept of cache, main memory, not the working memory (thread private data area) and main memory (JVM heap memory), they are just the Java memory model is an abstract concept is not real, so dividing the memory of the Java memory model for hardware memory and does not have any effect.

In the Java memory model, both the working memory and the main memory can be stored in the main memory, cache, or register of the hardware, so the Java memory model and the hardware memory model are an abstraction and the intersection of real physical hardware. The diagram is as follows:

Memory interaction

The Java memory model defines a specific implementation protocol for variable copy and synchronous write back between the main memory and the working memory. The protocol is mainly completed by eight operations. Different virtual machines must ensure that operations on each base data type are atomic and non-divisible. (Exceptions are made for long and double variables on some platforms. Although long and double are not required to be atomic in the JVM specification, the specification recommends that all JVMS be atomic. The actual JVM also basically realizes atomicity), the 8 operations are as follows:

  • Lock: A variable that acts on main memory and identifies a variable as a thread-exclusive state.
  • Unlock: Applies to a main memory variable. It releases a locked variable before it can be locked by another thread.
  • Read: Acts on a main memory variable, transferring 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 the value of the variable from main memory through the read operation into a copy of the variable in working memory.
  • Use: variable applied to working memory, passing the value of a variable in working memory to the execution engine. This operation is performed 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 transfers the value of a variable in working memory to main memory for subsequent write operations.
  • Write: a variable operating on main memory that places the value of a variable in main memory obtained from the working memory by the store operation.

The interaction diagram of thread, working memory and main memory for these 8 operations is as follows:

If a variable is copied from main memory to working memory, read and load operations are performed sequentially. If a variable is synchronized from working memory back to main memory, store and write operations are performed sequentially, not sequentially. This means that other operations can be inserted between the two operations, such as read 1, read 2, load 2, load 1 when accessing variables 1 and 2 in main memory.

In addition, the Java memory model imposes additional constraints on these eight operations:

  • Only read and load, store and write operations are allowed in pairs.
  • The thread is not allowed to discard its most recent assign operation, which means that after a variable is changed in working memory, it must be written back to main memory synchronously.
  • Do not allow threads to write variables that have not been assigned back to main memory synchronously.
  • A new variable can only be created in the main memory. Uninitialized variables cannot be used in the working memory. That is, the load and assign operations must be performed before the use and store operations are performed on a variable.
  • A variable can be locked by only one thread at a time. Once a variable is locked, it can be locked by the same thread several times. After multiple unlock operations are performed, the variable is unlocked only when the same unlock operations are performed.
  • A lock operation on a variable will empty its value in working memory, so it needs to be re-initialized by load or assign before the execution engine can use the variable.
  • Before an UNLOCK operation can be performed on a variable, the variable must be synchronized back to main memory (store, write).
  • It is not allowed to unlock a variable that has not been previously locked by a lock operation, nor is it allowed to unlock a variable that has been locked by another thread.

Three characteristics of the Java memory model

The Java memory model has always been built around how atomicity, visibility, and orderliness are handled in concurrent processes.

Atomicity

What is atomicity? Atomicity means that an operation can’t be broken, it can’t be separated, and in multithreading it means that once one thread starts performing an operation, it can’t be interfered with by other threads.

The operations directly used by the Java memory model to guarantee atomic variables include use, read, load, assign, Store, and write, and we can generally assume that access to basic Java data types is atomic (except for long and double, which we’ve already covered). The Java memory model also provides for lock and UNLOCK if the user wants to operate on a larger scale to ensure atomicity, but these operations are not directly open to the user. Instead, they provide two higher-level bytecode instructions: Monitorenter and Moniterexit, which correspond to the Synchronized keyword in Java code, are atomic operations between synchronized blocks.

Visibility

Visibility means that when one thread makes a change to a variable, other threads are immediately aware of the change.

The Java memory model achieves visibility through main memory as a delivery medium by synchronizing changes to variables and writing the new values back to main memory, flushing the values from main memory before reading. Either a regular or volatile modified variables are like that, the only difference is a volatile variable after being modified will immediately write back to the main memory, and will be back to main memory read when reading the latest value, and the common variables are being revised will be stored in working memory, and then from the working memory write back into main memory, A read reads a copy of the variable from working memory.

In addition to volatile, the synchronized and final keywords also provide visibility. Synchronized is visible because changes to a variable must be written back to main memory (store and write) before an unlock operation is performed on the variable. Final fields, on the other hand, are available to other threads once a final field has been initialized and cannot be changed.

Orderliness

As mentioned earlier, the processor performs out-of-order optimization, also known as reordering optimization, on program code while performing operations. Similarly, instruction reordering optimizations exist in the JVM, which would not be a problem in a single thread, but could be problematic in a multithreaded environment, because instruction optimizations in thread 1 could affect a state in thread 2.

Java provides the volatile and synchronized keywords to ensure order between threads. Volatile is implemented by its own prohibitive instruction reordering semantics, whereas synchronized is implemented by the rule that only one thread can lock a variable at a time, which is why synchronized blocks can only enter the same lock serially.

It can realize these three features in Java multithreading. Therefore, it has long been known that many people directly use synchronized to complete multithreading concurrent operations. However, using synchronized built-in lock will block the thread that needs but does not obtain the built-in lock, and the thread in Java corresponds to the native thread in the operating system one by one. Therefore, when a thread is blocked by synchronized built-in lock, the system will switch from user mode to kernel mode to perform blocking operation. This operation is very time consuming.

So much for the Java memory model, the next article will continue with a more lightweight synchronization implementation: the volatile keyword, as well as the memory barriers involved in the volatile implementation.