Overview of Java Memory Model (JMM)

Java memory model (java-memory-model)

Transfer from jenkov

The Java memory model specifies how the Java virtual machine is used with the computer's memory (RAM). The Java virtual machine is a model of the entire computer, so the model naturally includes a memory model-AKA Java memory model.

If you want to properly design a program with concurrent behavior, it is very important to understand the Java memory model. The Java memory model specifies how and when different threads see the values ​​written to shared variables by other threads, and how to synchronize access to shared variables when necessary.

The original Java memory model was insufficient, so the Java memory model was revised in Java 1.5. This version of the Java memory model is still used in Java (Java 14+) today.

Internal Java memory model

The Java memory model used internally by the JVM allocates memory between the thread stack and the heap. This figure illustrates the Java memory model from a logical perspective:

Insert picture description here
Each thread running in the Java virtual machine has its own thread stack. The thread stack contains information about which methods the thread called to reach the current execution point. I call it the "call stack". When a thread executes its code, the call stack changes.

The thread stack also contains all the local variables of each method being executed (all methods on the call stack). A thread can only access its own thread stack. Local variables created by a thread are invisible to all other threads except the creating thread. Even if the code executed by the two threads is exactly the same, the two threads will still create local variables of the code in their respective thread stacks. Therefore, each thread has its own version of each local variable.

All local variables of primitive types (boolean, byte, short, char, int, long, float, double) are completely stored on the thread stack, so they are not visible to other threads. A thread can pass a copy of a main variable to another thread, but it cannot share the original local variable itself.

The heap contains all objects created in a Java application, regardless of the thread that created the object. This includes primitive types (such as object versions Byte, Integer, Long, etc.). It doesn't matter whether you create an object and assign it to a local variable, or create it as a member variable of another object, the object is still stored in the heap.

This is a diagram illustrating the call stack, the local variables stored on the thread stack, and the objects stored on the heap:
Insert picture description here
local variables can be primitive types, in which case it remains completely in the thread stack.

Local variables can also be references to objects. In this case, the reference (local variable) is stored in the thread stack, but the object itself (if stored in the heap).

An object may contain methods, and these methods may contain local variables. These local variables are also stored in the thread stack, even if the object to which the method belongs is stored in the heap.

The member variables of the object are stored in the heap together with the object itself. This is true when the member variable is a primitive type and when it is a reference to an object.

Static class variables are also stored in the heap along with the class definition.

All threads referencing the object can access the object on the heap. When a thread can access an object, it can also access the member variables of the object. If two threads call a method on the same object at the same time, they will both have access to the member variables of the object, but each thread will have its own copy of the local variables.

This is a diagram illustrating the above points:
Insert picture description here
two threads have a set of local variables. One of the local variables (Local Variable 2) points to a shared object (Object 3) on the heap. These two threads each have different references to the same object. Their references are local variables, so they are stored in the thread stack of each thread (on each thread). However, two different references point to the same object on the heap.

Notice how the shared object (object 3) references object 2 and object 4 as member variables (as shown by the arrows from object 3 to object 2 and object 4). Through these member variable references in object 3, two threads can access object 2 and object 4.

The figure also shows a local variable that points to two different objects on the heap. In this case, the reference points to two different objects (object 1 and object 5), not the same object. In theory, if two threads both reference two objects, both threads can access object 1 and object 5. But in the figure above, each thread references only one of the two objects.

So, what kind of Java code might cause the memory graph above? Well, the code is as simple as the following code:

public class MyRunnable implements Runnable() {
    
    

    public void run() {
    
    
        methodOne();
    }

    public void methodOne() {
    
    
        int localVariable1 = 45;

        MySharedObject localVariable2 =
            MySharedObject.sharedInstance;

        //... do more with local variables.

        methodTwo();
    }

    public void methodTwo() {
    
    
        Integer localVariable1 = new Integer(99);

        //... do more with local variable.
    }
}

public class MySharedObject {
    
    

    //static variable pointing to instance of MySharedObject

    public static final MySharedObject sharedInstance =
        new MySharedObject();


    //member variables pointing to two objects on the heap

    public Integer object2 = new Integer(22);
    public Integer object4 = new Integer(44);

    public long member1 = 12345;
    public long member2 = 67890;
}

If two threads are executing the run() method, the result will be as before. The run() method calls methodOne() and methodOne() calls methodTwo().

methodOne() declares a basic local variable (localVariable1 type int) and a local variable, which is an object reference (localVariable2).

Each thread executing methodOne() will create its own copy, localVariable1 and localVariable2 on their own thread stack. These localVariable1 variables will be completely separated from each other and only exist in the thread stack of each thread. One thread cannot see the changes made by another thread to its copy localVariable1.

Each thread of execution methodOne() will also create its own copy of localVariable2. However, two different copies of the two localVariable2 end up pointing to the same object on the heap. This code sets localVariable2 to point to the object referenced by the static variable. There is only one copy of a static variable, and this copy is stored in the heap. Therefore, both localVariable2 end copies point to the same instance pointed to by the MySharedObject static variable. The MySharedObject instance is also stored in the heap. It corresponds to object 3 in the figure above.

Note that the MySharedObject class also contains two member variables. The member variable itself is stored in the heap together with the object. These two member variables point to two other Integer objects. These Integer objects correspond to object 2 and object 4 in the figure above.

Also notice how methodTwo() creates a local variable named localVariable1. This local variable is an object reference Integer to the object. This method sets the localVariable1 reference to point to the new Integer instance. The localVariable1 reference will be stored in a copy of methodTwo() in each thread of execution. The two objects instantiated by Integer will be stored on the heap, but since the method Integer will create a new object each time it is executed, the two threads executing this method will create separate Integer instances. The object methodTwo() created inside Integer corresponds to object 1 and object 5 in the above figure.

Also note that the two member variables in the MySharedObject type class, long they are primitive types. Since these variables are member variables, they are still stored in the heap together with the object. Only local variables are stored on the thread stack.

Hardware memory architecture

Modern hardware memory architecture is different from the internal Java memory model. It is also important to understand the hardware memory architecture and understand how the Java memory model works with it. This section describes common hardware memory architectures, and the next section describes how the Java memory model works with it.

This is a simplified diagram of modern computer hardware architecture:
Insert picture description here

Modern computers usually contain 2 or more CPUs. Some of these CPUs may also have multiple cores. The point is that on modern computers with 2 or more CPUs, multiple threads may run simultaneously. Each CPU can run a thread at any given time. This means that if the Java application is multi-threaded, each CPU may run a thread in the Java application at the same time (concurrently).

Each CPU contains a set of registers, which are essentially CPU memory. The CPU can perform operations on these registers much faster than the variables in the main memory. This is because the CPU can access these registers faster than the main memory.

Each CPU may also have a CPU cache storage layer. In fact, most modern CPUs have a certain size of cache layer. The CPU can access its cache faster than the main memory, but it is generally not as fast as it can access internal registers. Therefore, the CPU cache memory is located between the internal registers and the speed of the main memory. Some CPUs may have multiple cache layers (level 1 and level 2), but it is not important to understand how the Java memory model interacts with memory. It is important to know that the CPU can have some kind of cache layer.

The computer also contains a main storage area (RAM). All CPUs can access the main memory. The main storage area is usually much larger than the CPU cache.

Usually, when the CPU needs to access the main memory, it reads part of the main memory into its CPU cache. It can even read part of the cache into its internal registers and then perform operations on it. When the CPU needs to write the result back to the main memory, it will flush the value from its internal register to the cache, and then flush the value back to the main memory at some point.

When the CPU needs to store other content in the cache, it usually flushes the value stored in the cache back to the main memory. The CPU cache can write data to part of its memory at a time and refresh part of its memory at a time. It does not have to read/write the full cache every time it is updated. Generally, the cache is updated in smaller blocks of memory called "cache lines." One or more cache lines can be read into the cache memory, and one or more cache lines can be flushed back to the main memory again.

Bridging the gap between Java memory model and hardware memory architecture

As mentioned earlier, the Java memory model and hardware memory architecture are different. The hardware memory architecture does not distinguish between thread stack and heap. On hardware, the thread stack and heap are located in main memory. Part of the thread stack and heap may sometimes appear in the CPU cache and internal CPU registers. The following figure illustrates this:
Insert picture description here
When objects and variables can be stored in various storage areas of the computer, certain problems may occur. The two main issues are:

  • Thread updates (writes) to the visibility of shared variables.
  • Race conditions when reading, checking and writing shared variables.

These two issues will be explained in the following sections.

Visibility of shared objects

If two or more threads share an object without using the volatile declaration or synchronization correctly, the updates made by one thread to the shared object may not be visible to other threads.

Imagine that the shared object was initially stored in main memory. Then, the thread running on the CPU one reads the shared object into its CPU cache. There, it changed the shared library. As long as the CPU cache is not flushed back to main memory, threads running on other CPUs cannot see the changed version of the shared object. In this way, each thread can eventually have its own copy of the shared library, with each copy in a different CPU cache.

The figure below illustrates this situation. A thread running on the left CPU copies the shared library into its CPU cache and changes its count variable to 2. The other threads running on the right CPU cannot see this change because count has not flushed the update back to main memory.
Insert picture description here

To solve this problem, you can use Java's volatile keyword . The volatile keyword can ensure that a given variable is always written back to the main memory when it is directly read and updated from the main memory.

Competition conditions

If two or more threads share an object, and more than one thread updates variables in the shared object, a race condition may occur .

Imagine whether thread A reads the variable of the count shared library into its CPU cache. Also imagine that thread B has the same function, but it is in a different CPU cache. Now, thread A adds a count, and thread B performs the same operation. Now var1 has been increased twice, once per CPU cache.

If these increments are performed sequentially, the variable count will be incremented twice, and the original value + 2 will be written back to the main memory.

However, these two increments are executed simultaneously without proper synchronization. No matter which thread A or B writes its updated version back to main memory, although there are two increments, the updated value is only 1 higher than the original value.

This figure illustrates the occurrence of the race condition problem described above:
Insert picture description here
To solve this problem, you can use the Java synchronization block . The synchronization block guarantees that only one thread can enter a given critical part of the code at any given time. The synchronization block also guarantees that all variables accessed in the synchronization block will be read from the main memory. When the thread exits the synchronization block, all updated variables will be flushed back to the main memory again, regardless of whether the variable is declared volatile.

Guess you like

Origin blog.csdn.net/e891377/article/details/108686228