In the Java JVM series, a friend asked why a JVM when the Java Virtual Machine already takes care of it for us. Similarly, learning the Java memory model has the same problem, why do you want to learn the Java memory model. The answer is the same: it allows us to better understand the underlying principles and write more efficient code.

As far as the Java memory model is concerned, it is a prerequisite for an in-depth understanding of Java concurrent programming. It is of great benefit to thread safety, synchronous and asynchronous processing in the following multithreading.

Hardware memory architecture

Before learning about the Java memory model, take a look at the computer hardware memory model. We know that processors differ in speed by orders of magnitude from computer memory devices. You can’t have the processor always waiting for the computer’s storage device so that the advantages of the processor can’t be realized.

Therefore, in order to “squeeze” the performance of processing and achieve “high concurrency”, a cache is added between the processor and the storage device as a buffer.

Copy the data required by the operation to the cache, so that the operation can be fast. When the operation is complete, the result from the cache is written to main memory, so that the processor does not have to wait for a read or write operation from main memory.

Each processor has its own cache and concurrently operates the same main memory. When multiple processors operate the main memory at the same time, data inconsistency may occur. Therefore, the cache consistency protocol is required to ensure data consistency. For example, MSI, MESI, etc.

Java Memory Model

The Java Memory Model (JMM) is also known as the Java Memory Model. It is used to mask memory access differences between various hardware and operating systems so that Java programs can achieve consistent memory access effects across all platforms.

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 local memory that stores copies of shared variables that the thread reads/writes to. Local memory is an abstract concept of the JMM and does not really exist. It covers caching, write buffers, registers, and other hardware and compiler optimizations.

JMM and Java memory structure are not at the same level of memory partition, and they are basically unrelated. If you have to, then from the definition of variables, main memory, and working memory, main memory mainly corresponds to the object instance data part of the Java heap, working memory corresponds to part of the virtual machine stack.

Main memory: Java instance objects are stored in main memory. All thread-created instance objects are stored in main memory, whether they are member variables or local variables (also called local variables) in methods, as well as shared class information, constants, and static variables. Shared data areas where multiple threads access the same variable may cause thread-safety problems.

Working memory: Each thread has access only to its own working memory, that is, local variables in a thread are invisible to other threads, even if both threads are executing the same piece of code. They also create local variables in their own working memory that belong to the current thread, as well as bytecode line number indicators and relevant Native method information. Since working memory is private data for each thread, threads cannot access working memory with each other, so data stored in working memory is not thread-safe.

The direct comparison between JMM model and hardware model can be simplified as follows:

Interoperation between memory

The working memory of a thread holds a copy of the main memory of variables used by the thread. All operations on variables must be performed in the working memory. Different threads cannot directly access variables in each other’s working memory, and the transfer of variable values between threads needs to be completed through main memory.

As shown in the figure above, local memory A and B have A copy of the shared variable x in main memory, both with an initial value of 0. Thread A updates x to 1 and stores it in local memory A. When thread A and thread B need to communicate, thread A will first flush the x=1 value in local memory to main memory, and the x value in main memory will become 1. Thread B then reads the updated x value from main memory, and thread B’s local memory x value is changed to 1.

During this interaction, the Java memory model defines eight operations to be performed, and the virtual machine implementation must ensure that each operation is atomic and non-separable (with the exception of the double and long types).

  • Lock: A variable that acts on main memory to identify a variable as being exclusive to a thread.
  • Unlock: A variable that acts on main memory. It releases a locked variable so that it can be locked by another thread.
  • Read: A variable that acts on main memory to transfer the value of a variable from main memory to the thread’s working memory for subsequent load actions.
  • Load: a variable that acts on working memory, putting the value of the variable obtained from main memory by the read operation into the working memory copy of the variable.
  • Use: a variable that acts on working memory. It passes the value of a variable in working memory to the execution engine, which is performed whenever the virtual machine gets a bytecode instruction that requires the value of the variable.
  • Assign: a variable that acts on the working memory. It assigns a value received from the execution engine to the variable in the working memory. This operation is performed whenever the virtual opportunity comes to a bytecode instruction that assigns a value to the variable.
  • Store: a variable that acts on working memory to transfer the value of a variable in working memory to main memory for subsequent write operations.
  • Write: A variable that acts on main memory and places the value of the variable obtained from the working memory by the store operation into the main memory variable.

If a variable needs to be copied from main memory to working memory, it needs to be read and load sequentially. If it needs to be synchronized from working memory back to main memory, it needs to be store and write sequentially. Note that the Java memory model only requires that the above two operations be performed sequentially; there is no guarantee that they will be performed consecutively. In other words, other instructions can be inserted between read and load, store and write. For example, when accessing variables a and B in main memory, one possible order is read a, read B, load B, load a. In addition, the Java memory model specifies that the following rules must be met when performing the basic operations described in the preceding 8.

  • Read and one of the load, store, and write operations alone are not allowed. That is, a variable is not allowed to be read from main memory but not accepted by working memory, or a variable is written back from working memory but not accepted by main memory.
  • A thread is not allowed to discard its most recent assign operation, that is, after a variable has changed in working memory, the change must be synchronized back to main memory.
  • A thread is not allowed to synchronize data from the thread’s working memory back to main memory for no reason (no assign operation has occurred).
  • A new variable can only be “born” in main memory. It is not allowed to use a variable that has not been initialized (load or assign) in working memory. In other words, a variable must be assigned or loaded before it is used or stored.
  • Only one thread can lock a variable at a time, but the lock operation can be repeated by the same thread many times. After multiple lock operations, the variable can be unlocked only by performing the unlock operation for the same number of times.
  • If you lock a variable, it will clear the value of the variable in the working memory, and you will need to load or assign the variable again to initialize it before the execution engine can use it.
  • You are not allowed to unlock a variable that has not been locked before, and you are not allowed to unlock a variable that has been locked by another thread.
  • Before you can unlock a variable, you must synchronize it back to main memory (store, write).

Special rules for long and double variables

The Java memory model requires that all eight operations lock, unlock, read, load, assign, use, Store, and write be atomic, but for 64-bit data types (long or double), the model defines a relatively loose rule. This allows a VM to divide a read/write operation on 64-bit data that has not been volatile into two 32-bit operations. This allows a VM to select atomicity (long and double) for load, Store, read, and write operations that do not guarantee 64-bit data types.

If double or long is not declared as volatile in the case of multithreading, it is possible to have a “half variable” value, which is neither the original value nor the modified value.

Although the Java specification allows for the above implementation, commercial virtual machines are largely atomistic, so reading “half a variable” is almost never the case in everyday use.

summary

This tutorial focuses on the Java memory model and the steps and operations of memory interaction. The next article will focus on a few characteristics and principles involved in the Java memory model. Welcome to follow the wechat public account “Program New Horizon”, the first time to get the latest article updates.

Java Memory Model (JMM) In Detail

The Interviewer series:

  • The JVM Internal-memory Structure in Detail
  • Interviewer, stop asking me about Java GC.
  • “Interviewer, Java8 JVM memory structure changed, permanent generation to meta space”
  • Interviewer, Stop asking me about the Java Garbage Collector
  • Java Virtual Machine class Loaders and Parent Delegation
  • Java Memory Model (JMM) Details


New horizons for programs