Introduction to the

The Java memory model is a higher level abstraction of the hardware memory model. It shields the difference between hardware and operating system access, and ensures that the Java program access to memory on various platforms can achieve the same effect.

Hardware memory model

Before we dive into Java’s memory model, it’s worth taking a look at some of the hardware aspects.

In modern computer hardware system, the CPU speed is very fast, far more than it reads data from a storage medium, the speed of the storage medium has a lot of, such as disk, CD, network CARDS, memory, etc., these storage medium has an obvious features – the closer distance CPU storage medium are relatively expensive faster and faster, Storage media farther away from the CPU tend to be bigger, cheaper and slower.

So, in the process of application, most of the CPU time is wasted in the disk IO, network communication, database access, if you don’t want to let the CPU there waiting in vain, we need to find ways to put the CPU computing power squeezing out, otherwise it will cause a lot of waste, and let the CPU to handle multiple tasks at the same time is the easiest way to think of, It’s also proven to be a very effective way to squeeze, which is what we call “concurrent execution.”

However, it is not easy to make the CPU perform multiple tasks concurrently, because all operations cannot be completed by the CPU alone, and often need to interact with memory, such as reading operation data and storing operation results.

As mentioned earlier, cpu-memory interactions tend to be slow, so we need to find a way to create a connection between the CPU and memory, so that they reach a balance, so that computing can be done quickly. This connection is often referred to as “caching.”

The speed of cache is very close to CPU, but its introduction brings new problems. Modern CPUS tend to have multiple cores, each core has its own cache, and there is no time slice competition between multiple cores. They can execute in parallel. How to ensure that these caches and main memory data consistency becomes a problem.

In order to solve the problem of cache coherence, multiple cores should follow some agreement, when access to the cache in the read and write operations to operate according to the agreement, the agreement with MSI, msci, MOSI, they define when to access the data in the cache and when to let cache invalidation, the basic principles such as when to access data in main memory.

With the continuous improvement of CPU capacity, one level of cache can not meet the requirements, and gradually derived from multi-level cache.

The CPU cache can be divided into level-1 cache (L1), level-2 cache (L2), and level-3 cache (L3) according to the order in which data is read and the tightness of the CPU. The data stored in each level-2 cache is part of the data stored in the next level-2 cache.

The technical difficulty and production cost of these three caches are relatively decreasing, and the capacity is also relatively increasing.

So, with multi-level caching, the program runs like this:

When the CPU to read a data from the first level cache lookup, if not found in the second level cache lookup, if not found again from level 3 cache lookup, if not found from the main memory to find again, and then to find the data load in turn to multistage cache, next time related to the use of data directly from the cache lookup.

And loaded into the data in the cache which is load which is not to say that use, load continuous data in the memory, but in general is to load a continuous 64 bytes, so, if a visit to a long array, when a value in the array are loaded into the cache, the other seven elements can be loaded into the cache, This is the concept of “cache rows”.

Although the cache line can greatly improve the efficiency of the program, but in the process of multi-thread access to the shared variable, it brings a new problem, which is very famous “pseudo-sharing”.

As for the problem of false sharing, we will not expand on it here. If you are interested, you can see tongge’s previous post [Miscellaneous Discussion on what is false sharing?] Chapter related content.

In addition, in order to make the operation unit can sufficiently use of CPU, the CPU is likely to enter the code in order to perform optimization, and then after calculating the out-of-order execution as a result of restructuring, ensure that the results are consistent with the results of the order, but does not guarantee all the statements in the program computing the order input sequence is consistent with the code, therefore, If one computing task depends on the results of another, the sequentiality is not guaranteed by the sequencing of the code.

Similar to out-of-order execution optimization for the CPU, the Just-in-time compiler for the Java virtual machine has a similar instruction reordering optimization.

To address the above mentioned problems of multiple cache read/write consistency and out-of-order sorting optimization, there is a memory model that defines the behavior of multithreaded read/write operations in a shared memory system.

Java memory model

The Java Memory Model (JMM) is a higher level abstraction based on the hardware Memory Model. It shields the difference of Memory access between various hardware and operating systems, so that Java programs can achieve consistent concurrent effect on various platforms.

The Java memory model defines the access rules for variables in a program, the low-level details of storing variables in and out of memory in the virtual machine. Variables in this context include instance fields, static fields, but not local variables and method parameters, because they are thread private, they are not shared, and there are naturally no race issues.

For better performance, the Java memory model does not limit the execution engine’s ability to use the processor’s specific registers or caches to interact with main memory, or the ability of the just-in-time compiler to adjust the order in which code is executed, for example.

The Java memory model specifies that all variables are stored in main memory, which is analogous to the hardware name, but refers only to a portion of memory in the virtual machine.

In addition to main memory, each thread has its own working memory, an analogy to a CPU’s cache. Working memory holds the main memory of the variables of the thread used to copy the copy, thread to the operation of the variables must be done in working memory, including reading and assignment, etc., and cannot be directly read/write variables in main memory, between different threads cannot directly access the other variables in the working memory, a variable’s value transfer between threads must be done through the main memory.

The relationship among threads, working memory, and main memory is shown in the following figure:

Note that the main memory, working memory and the stack in the Java virtual machine memory partition are different levels of memory partition, and if they must be closely related, the main memory corresponds mainly to the instance part of the object in the heap, while the working memory corresponds mainly to the part of the virtual machine stack.

At a lower level, main memory corresponds primarily to the hardware memory portion, and working memory corresponds primarily, but not exclusively, to the CACHE and registers portion of the CPU. Main memory can also be in caches and registers, and working memory can also be in hardware memory.

Interoperation between memory

The Java Memory model defines the following eight specific operations to accomplish the specific interaction protocol between main and working memory:

(1) Lock, a variable that acts on main memory and identifies the variable in main memory as a thread-exclusive state;

(2) UNLOCK, a variable that operates on the main memory. It releases the locked variable so that it can be locked by other threads.

(3) Read, a variable operating on main memory that transfers a variable from main memory to working memory for subsequent load operations;

(4) a variable that acts on working memory. It loads variables from main memory into a copy of the variables in working memory.

A variable in working memory that passes a variable in working memory to the execution engine. This operation will be performed whenever the virtual machine reaches a bytecode instruction that requires the value of the variable to be used.

Assign a variable to the working memory. This operation assigns a variable received from the execution engine to the working memory variable whenever the virtual machine accesses a bytecode instruction that assigns a value to the variable.

(7) 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 writes the value of a variable from the working memory to a variable in main memory.

If a variable is copied from main memory to working memory, read and load operations are performed sequentially. Similarly, if a variable is synchronized from working memory back to main memory, store and write operations are performed sequentially. Note that it says sequential, not sequential, that other operations can be inserted between read and load, or between store and write. For example, accesses to variables A and B in main memory can be performed in the following order:

Read A -> read B -> Load B -> Load A.

In addition, the Java memory model defines the basic rules for performing the eight operations described above:

(1) One of the read and load, store and write operations is not allowed to appear alone, that is, it is not allowed to read from the main memory but not accepted by the working memory, or write back from the working memory but not accepted by the main memory;

(2) a thread is not allowed to discard its recent assign operation. That is, a variable that changes in the working memory must be synchronized back to the main memory.

(3) it is not allowed for a thread to synchronize a variable from the working memory to the main memory without a cause (i.e., without a assign operation).

(4) A new variable must be created in the main memory. It is not allowed to use a variable that has not been assigned before. In other words, the use and store operations on a variable must be load and assign before.

(5) A variable can only be locked by one thread at a time, but the lock operation can be performed by the same thread several times. After the lock operation is performed for several times, the variable can be unlocked only after the same number of UNLOCK operations are performed.

(6) If the lock operation is performed on a variable, the value of the variable will be emptied from the working memory. Before the variable can be used by the execution engine, the load or assign operation needs to be performed again to initialize the value of the variable.

(7) If a variable is not locked by a lock operation, it is not allowed to unlock it, nor is it allowed to unlock a variable that is locked by another thread.

(8) Before performing an UNLOCK operation on a variable, the variable must be synchronized back to the main memory, that is, store and write operations.

Note that lock and unlock are the basis for synchronized. Java does not open lock and unlock operations directly to the user, but provides two higher-level instructions to implicitly use them: MoniterEnter and MoniterExit.

Atomicity, visibility, order

The Java memory model is designed to solve the problem of consistency of shared variables in multi-threaded environments. What does consistency include?

Consistency consists of three main features: atomicity, visibility, and orderliness. Let’s look at how the Java memory model implements these three features.

(1) atomicity

Atomicity means that once an operation is started, it will continue to the end without being interrupted by other threads. This operation can be a single operation or multiple operations.

The atomic operations directly guaranteed by the Java memory model include read, load, user, assign, Store, and write. We can generally assume that reads and writes of primitive variables are atomic.

If an application requires a wider range of atomicity, the Java memory model also provides lock and unlock operations to meet this requirement, and although these cannot be used directly, they can be used with a more specific implementation of synchronized.

Thus, operations between synchronized blocks are also atomic.

(2) Visibility

Visibility means that when one thread changes the value of a shared variable, other threads can immediately sense the change.

The Java memory model is implemented by synchronizing changes back to main memory and flushing variable values from main memory before variables are read. It is dependent on main memory, whether common or volatile.

The main differences between plain and volatile variables are whether they are synchronized back to main memory immediately after modification, and whether they are flushed from main memory immediately before each read. Thus we can say that volatile variables guarantee visibility in a multithreaded environment, but ordinary variables do not.

In addition to volatile, two other keywords ensure visibility: synchronized and final.

The visibility of synchronized is obtained by the rule that a variable must be synchronized back into main memory before an UNLOCK operation can be performed, that is, store and write operations.

Final visibility means that once a final field is initialized in the constructor, it can be seen by other threads.

(3) Orderliness

The natural orderliness of Java programs can be summed up as follows: if you look at this thread, all operations are ordered; If viewed from another thread, all operations are out of order.

The first half of the sentence refers to the sequential semantics in the thread, and the second half refers to the “instruction reordering” phenomenon and “working memory and main memory synchronization delay” phenomenon.

Java provides the keywords volatile and synchronized to ensure order.

Volatile is inherently ordered because it forbids reordering.

The order of synchronized is obtained by the rule that only one thread can lock a variable at a time.

Happens-before principle

If the order of the Java memory model were to rely solely on volatile and synchronized, some operations would be verbose, but we don’t feel that way when we write Java concurrent code, because the Java language naturally has a preemptive principle that is very important. With this principle we can easily determine whether two operations in a concurrent environment are likely to have conflict problems.

If operation A occurs before operation B, operation B can perceive the impact of operation A, such as changing the value of variables in the shared memory, sending messages, and invoking methods.

Let’s take a look at the first occurrence principles defined by the Java memory model:

(1) The principle of procedural order

In a thread, the program is executed in the order in which it is written, with the operations written first occurring before those written later, precisely controlling flow order rather than code order because of branching, loops, and so on.

(2) The principle of monitor locking

An UNLOCK operation occurs first after a lock operation on the same lock.

(3) Volatile

A write to a volatile variable occurs first after a read to that variable.

(4) Thread starting principle

The start() operation on a thread precedes any operation within the thread.

(5) Thread termination principle

All operations in a Thread occur when Thread termination is detected. You can check whether the Thread has terminated by the return values of thread.join () and thread.isalive ().

(6) Thread interrupt principle

A call to interrupt() on a Thread occurs first when an interrupt event is detected in the Thread’s code, which can be checked by thread.interrupted ().

(7) Object termination principle

The completion of an object’s initialization (the end of constructor execution) occurs first at the beginning of its Finalize () method.

(8) Principle of transitivity

If operation A precedes operation B and operation B precedes operation C, then operation A precedes operation C.

Here said “antecedent” and “on the time of the antecedent” there is no necessary relationship.

For example, the following code:

int a = 0;

// operation A: assign to thread 1 pairs
a = 1;

Thread 2 retrieves the value of a

int b = a;
Copy the code

If thread 1 assigns a value to a in chronological order, and thread 2 gets a value from A, does that mean that operation A took place before operation B?

Obviously not, because thread 2 might still be reading from its working memory, or thread 1 might not be flushing a back to main memory, so thread 2 might still be reading 0.

Therefore, “temporal preoccurrence” is not necessarily “antecedent”.

Here’s another example:

// In the same thread
int i = 1;

int j = 2;
Copy the code

Int I = 1; Int j = 2; , but due to processor optimization, int j = 2 May result; Execute first, but this does not affect the correctness of the first occurrence principle, because we are not aware of it in this thread.

Therefore, “antecedent” is not necessarily “time prior”.

conclusion

(1) The hardware memory architecture makes it necessary to build a memory model to ensure the correctness of shared memory access in multi-threaded environment;

(2) The Java memory model defines rules to ensure the consistency of shared variables in multi-threaded environment;

(3) The Java memory model provides eight operations for working memory to interact with main memory: Lock, unlock, read, Load, use, assign, Store, and write.

(4) The Java memory model provides some implementations of atomicity, visibility, and orderliness;

(5) The eight leading principles: program order principle, monitor locking principle, volatile principle, thread start principle, thread termination principle, thread interrupt principle, object termination principle, transitivity principle;

(6) Antecedent occurrence is not equal to antecedent occurrence in time;

eggs

Java memory model is a very important concept in Java, understanding it is very helpful for us to write multithreaded code, understand the nature of multithreading, the author here sorted out some good information to provide you.

In-depth Understanding of the Java Virtual Machine

The Art of Concurrent Programming in Java

Understanding the Java Memory Model in Depth

Pay attention to my public number “Tong Elder brother read source code” reply “JMM” to receive the above three books.


Welcome to pay attention to my public number “Tong Elder brother read source code”, view more source code series articles, with Tong elder brother tour the ocean of source code.