Today’s sharing started, please give us more advice ~

Introduction: Communication between Java threads is completely transparent to programmers, and memory visibility problems can easily confuse Java programmers. This series of articles will demystify the Java memory model. This series of articles can be roughly divided into four parts, which are:

  • The Java Memory model introduces basic concepts related to the memory model.

  • Sequential consistency in the Java memory model, mainly introduces reordering and sequential consistency memory model.

  • Synchronization primitives, mainly introduces the memory semantics of three synchronization primitives (synchronized, volatile and final) and the implementation of reordering rules in the processor.

  • The design of Java memory model mainly introduces the design principle of Java memory model and its relationship with processor memory model and sequential consistency memory model.

The foundation of the Java memory model

1.1 Two key problems of the concurrent programming model

Two key issues need to be addressed in concurrent programming: how threads communicate with each other and how threads synchronize with each other (threads here are active entities that execute concurrently). Communication – the mechanism by which threads exchange information. In imperative programming, there are two communication mechanisms between threads: shared memory and messaging.

  • Shared memory: The common state of a program shared between threads, communicating implicitly by reading and writing to a common turntable in memory

  • Messaging: There is no common state between threads, and threads must communicate explicitly by sending messages

Synchronization – a mechanism used in a program to control the relative order in which key operations on different threads occur.

  • Shared memory: Synchronization is explicit because the programmer must explicitly specify that a method or piece of code needs to be executed mutually exclusive between threads

  • Messaging: Synchronization is implicit because the message must be sent before the message is received.

Conclusion:

Concurrency in Java is a shared memory model. Communication between Java threads is always implicit, and the whole communication process is completely transparent to the programmer. Java programmers writing multithreaded programs are likely to encounter all kinds of strange memory visibility problems if they do not understand how implicit communication between threads works.

1.2 Abstract structure of Java memory model

All instance fields, static fields, and array elements in Java are stored in heap memory, which is shared between threads (referred to in this article as “shared variables”). Local Variables, Method definition Parameters, and Exception Handler Parameters are not shared between threads, and they do not have memory visibility issues. Therefore, it is not affected by the memory model.

Communication between Java threads is controlled by the Java Memory Model (JMM), which determines when writes by one thread to a shared variable are visible to another thread. From an abstract point of view, 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 where it stores copies of shared variables to read/write. Local memory is an abstraction of the JMM and does not really exist. The JMM covers caching, write buffers, registers, and other hardware and compiler optimizations.

An abstract diagram of the Java memory model

From the diagram above, to communicate between thread A and thread B, two steps must be taken.

  1. Thread A flushes the updated variables from local memory A to main memory
  2. Thread B goes into main memory to read the shared variable that thread A has updated before

Schematic diagram of communication between threads

As shown in the figure above, local memory A and local memory B share A copy of variable X in main memory. Thread A temporarily stores the updated value of X (let’s say 1) in its own local memory A. When thread A and thread B need to communicate, thread A first flusher the modified X in its local memory to the main memory, and the X value in the main memory becomes 1. Thread B then goes to main memory to read thread A’s updated X value, and thread B’s local memory X value is also updated to 1.

Taken as A whole, these two steps are essentially thread A sending messages to thread B, and this communication must go through main memory. The JMM provides Java programmers with memory visibility assurance by controlling the interaction between main memory and local memory for each thread.

1.3 Reordering from source code to instructions

When executing a program, the compiler and processor often reorder instructions to improve performance. There are three types of reordering:

  1. Compiler optimized reordering. The compiler can rearrange the execution order of statements without changing the semantics of a single-threaded program.
  2. Instruction – level parallel reordering. Modern processors use instruction-level Parallelism (ILP) to overlap the execution of jump instructions. If there are no data dependencies, the processor can change the order in which statement correspondence and its instructions are executed.
  3. Memory system reordering. Because the processor uses caching and read/write buffers, this makes the load and store operations appear to be out of order.

The actual sequence of instructions executed from the final Java source code undergoes three reorders: 1 for compiler reorder, 2 and 3 for handler reorder.

Diagram of the sequence of instructions from source code to final execution

Reordering can cause memory visibility problems for multithreaded programs, and for compilers, the JMM’s compiler reordering rules prohibit certain types of compiler reordering (not all compiler reordering needs to be prohibited). For processor reordering, the JMM’s processor reordering rules require the Java compiler to insert a specific type of Memory barrier (or Memory Fence, as Intel calls it) to prohibit reordering of a particular type of processor when generating an instruction sequence.

The JMM is a language-level memory model that ensures consistent memory visibility for programmers across compilers and processor platforms by disallowing certain types of compiler reordering and processor reordering.

1.4 Write buffers and memory barriers

1.4.1 Writing buffer

Modern processors use write buffers to temporarily store data written to memory. Write buffer main functions:

  • This ensures that the instruction pipeline is running continuously and avoids delays caused by the processor pausing to wait for data to be written to memory.
  • It flushes the write buffer in a batch manner and merges multiple writes to the same address in the write buffer, reducing the footprint on the memory bus.

Reordering types allowed by common processors (Y- means two operations are allowed to reorder, N- means the processor does not allow two operations to reorder)

Note: Common processors allow Store-Load reordering; Common processors do not allow reordering operations that have data dependencies. An n-plus processor has a relatively strong processor memory model.

Since the write buffer is visible only to the processor in which it resides, this feature has a very important effect on the order in which memory operations are executed: the order in which the processor reads/writes to memory is not necessarily the same as the order in which the memory actually reads/writes.

For example:

Given that processors A and B perform memory accesses in parallel in program order, it is possible to end up with x=y=0 for the following reasons:

Schematic diagram of processor and memory interaction

Note: Processor A and processor B can simultaneously write shared variables to their own write buffers (A1 and B1), and then read another shared variable from memory (A2 and B2), and finally flush the dirty data stored in their write buffers to memory (A3 and B3). When executed in this sequence, the program results in x=y=0.

1.4.2 Memory barriers

To ensure memory visibility, the Java compiler inserts memory barrier instructions at the appropriate places to generate instruction sequences to prohibit reordering of a particular type of handler. The JMM classifies memory barrier instructions into four categories:

StoreLoad Barriers is a “universal barrier” that has the effect of the other three Barriers. Most modern processors support this barrier (other types of barriers are not necessarily supported by all processors). Performing this barrier can be expensive because the processor needs to Flush the contents of the Buffer Fully into memory.

1.5 happens-before profile

Starting with JDK1.5, Java uses the new jsr-133 memory model. Jsr-133 uses the concept of happens-before to illustrate memory visibility between operations. In the JMM, if the result of one operation needs to be visible to another, there must be a happens-before relationship between the two operations. The two operations here can be single-threaded or multi-threaded.

Happens-before rules:

  • Procedure order rule: For every action in a thread, happens-before any subsequent action in that thread.

  • Monitor lock rule: For a lock to be unlocked, happens-before is for the lock to be locked later.

  • Volatile variable rule: For writes to a volitale field, happens-before is used for any subsequent reads to the volatile field.

  • Transitivity: If A happens-before B, and B happens-before C, then A happens-before C.

Note:

A happens-before relationship between two actions does not mean that the previous action must be executed before the latter! Happens-before only requires that the former operation (the result of the execution) be available to the latter, and that the first is visiable to and ordered beofre the second.

The happens-before relationship to the JMM is illustrated

Happens-before relationship to JMM

A happens-before rule corresponds to one or more compiler handler reordering rules. The happens-before rule is straightforward for Java programmers, and it prevents them from having to learn complex reordering rules and how to implement them in order to understand the memory visibility guarantees provided by the JMM.

Today’s share has ended, please forgive and give advice!