Original article & Experience summary & from the university to A factory all the way sunshine vicissitudes

For details, please click www.codercc.com

1. Introduction of JMM

In the last article, I summarized the state transitions and some basic operations of threads. I already have a basic understanding of multithreading. If multithreaded programming were that simple, we wouldn’t have to bother to learn it. What is a thread-safe problem that can occur with a little inattention in multithreading? My understanding is that code is thread unsafe when it executes in multiple threads and does not match the expected correct result; otherwise, the code is thread safe. Although this answer doesn’t seem to yield much content, Google it. The definition seen in << Deeper Understanding of the Java Virtual Machine >>. The original text is as follows: when multiple threads access the same object, if don’t have to consider these threads in the runtime environment scheduling and alternate operation, also do not need to undertake additional synchronization, or any other coordinated operation in the caller, call the object’s behavior can obtain correct results, that the object is thread-safe.

The definition is a matter of opinion. The most important part of solving thread-safe problems is to understand where these two problems come from. At the heart of understanding them lies an understanding of the Java Memory Model (JMM).

In multithreading, multiple threads must work together to do the same thing, which generally involves communicating with each other about their state and current execution results. In addition, it involves reordering compiler instructions and reordering processor instructions for performance optimization. We’ll talk about that in a second.

2. Abstract structure of memory model

Collaborative communication between threads can analogy, collaboration between people, in real life, there is a popular online before “your mama calls you home for dinner”, is in the life scenes, for example, xiao Ming play outside, xiaoming mother cooking at home, cook dinner ready to call after xiao Ming home for dinner, so there are two ways:

Xiao Ming’s mother has to go to work in an emergency. At this time, her mobile phone has no electricity, so she stuck a note on the table, “The meal is ready, on…” When Xiao Ming came home, he saw the note and ate the food cooked by his mother as he wanted. Then, if Xiao Ming’s mother and Xiao Ming are regarded as two threads, the note is the shared variable of communication between the two threads, and the cooperation between the two threads is realized by reading and writing the shared variable.

There is another way, the mother’s mobile phone is still electricity, the mother in the rush to take the bus on the way to make a call to Xiao Ming, this way is the notification mechanism to complete cooperation. Similarly, it can be extended to inter-thread communication mechanisms.

There’s something to be learned from this example. Two problems need to be solved in concurrent programming: 1. How to communicate between threads; 2. 2. How to complete synchronization between threads (in this case, threads are active entities executing concurrently). Communication refers to the mechanism by which threads exchange information. There are two main types: shared memory and message passing. Here, the above two examples can be compared respectively. The Java memory model is a concurrent model of shared memory. Implicit communication between threads is mainly accomplished by reading and writing shared variables. Programmers who don’t understand Java’s shared memory model are bound to run into all sorts of problems with memory visibility when writing concurrent programs.

1. Which are shared variables

In a Java program, all instance fields, static fields, and array elements are stored in heap memory (accessible and shareable by all threads), while local variables, method definition parameters, and exception handler parameters are not shared between threads. Shared data can cause thread-safe problems, whereas non-shared data does not. The JVM runtime memory region is covered in a later article.

2.JMM abstract structure model

We know that CPU processing speed and main memory read and write speed are not on the same order of magnitude, to balance this huge gap, every CPU has a cache. Shared variables, therefore, will be placed in main memory, each thread has its own working memory, and will be located in the main memory the Shared variables in copy to their working memory, read and write operations are used in the working memory after a copy of the variable, and at some point to the working memory copy of the variable to write back to main memory. The JMM defines this approach at the level of abstraction, and it determines when a thread’s write to a shared variable is visible to other threads.

To complete communication between threads A and B, the following two steps are required:

  1. Thread A reads the shared variable from main memory into thread A’s working memory and operates on it, then writes the data back to main memory.
  2. Thread B reads the latest shared variable from main memory

Looking horizontally, thread A and thread B seem to be implicitly communicating through shared variables. An interesting problem with this is that if thread A updates the data and does not write it back to main memory in time, thread B reads the expired data, A “dirty read” phenomenon occurs. This can be resolved by a synchronization mechanism (which controls the relative order in which operations occur between different threads) or by the volatile keyword so that each volatile variable can be forcibly flushed into main memory, thus being visible to each thread.

3. The reorder

A good memory model actually loosens the rules of the processor and compiler, meaning that both software and hardware technologies are striving for the same goal: to achieve as much parallelism as possible without changing the outcome of program execution. The JMM minimizes constraints on the underlying layer, allowing it to leverage its strengths. As a result, compilers and processors often reorder instructions to improve performance when executing a program. General reordering can be divided into the following three types:

  1. Compiler optimized reorder. The compiler can rearrange the execution order of statements without changing the semantics of a single-threaded program.
  2. Parallel reorder at instruction level. Modern processors use instruction-level parallelism to overlap multiple instructions. If there is no data dependency, the processor can change the order in which the statement is executed to the corresponding machine instruction.
  3. Reordering of the memory system. Because the processor uses caching and read/write buffers, it can appear that load and store operations are being performed out of order.

As shown, 1 is a compiler reorder, while 2 and 3 are collectively referred to as a processor reorder. These reorders can lead to thread-safety problems, a classic example of which is the DCL problem, which will be discussed in detail in a future article. For compiler reordering, the JMM’s compiler reordering rules prohibit reordering for certain types of compiler. For processor reordering, the compiler will insert memory barrier instructions to prevent certain special processors from reordering when generating sequence of instructions.

So under what circumstances can you not reorder? Let’s talk about data dependencies. There is the following code:

Double PI = 3.14 //A

Double r = 1.0 //B

double area = pi * r * r //C

This is A code to calculate the area of A circle. Since there is no relationship between A and B, there will be no relationship to the final result, and the execution order between them can be reordered. Therefore, the execution order can be A->B->C or B->A->C, and the final result is 3.14, that is, there is no data dependence between A and B. The specific definition is: if two operations access the same variable, and one of the two operations is a write operation, then the two operations are data dependent. Read write; 2. Write after write; 3. The three operations are data dependent. Reordering will affect the final execution result. The compiler and processor comply with the data dependencies when reordering. The compiler and processor do not change the execution order of the two operations in which the data dependencies exist

Another interesting aspect is the as-if-serial semantics.

as-if-serial

The as-if-serial semantics mean that the execution result of a (single-threaded) program cannot be changed no matter how much reordering is done (by the compiler and processor to provide parallelism). The compiler, Runtime, and processor must all follow the AS-if-serial semantics. The as-if-serial semantics protect single-threaded programs. Compilers, Runtime, and processors that follow the AS-IF-serial semantics create the illusion for programmers who write single-threaded programs that they are executed in the order of the program. For example, in the above code to calculate the area of the circle, in A single thread, it will appear that the code is executed line by line in order. In fact, lines A and B do not have data dependence and may be reordered, that is, A and B are not executed in order. The AS-if-serial semantics let programmers not worry about reordering in a single thread interfering with them, nor do they have to worry about memory visibility issues.

4. Happens-before rules

The above content describes the reordering principle, one is the compiler reordering and the other is the processor reordering, if the programmer to understand the underlying implementation and the specific rules, then the programmer burden is too heavy, seriously affects the efficiency of concurrent programming. Thus, the JMM provides the programmer with six rules at the top, from which we can infer cross-thread memory visibility without having to understand the underlying reordering rules. Here are two aspects.

4.1 happens-before definition

The happens-before concept was first proposed by Leslie Lamport in his influential paper (Time, Clocks and the Ordering of Events in a Distributed System), If you’re interested, Google it. Jsr-133 uses the concept of happens-before to specify the order of execution between two operations. Because these two operations can be within a thread or between threads. Thus, the JMM can provide programmers with A guarantee of memory visibility across threads through happens-before relationships (if there is A happens-before relationship between A’s write operation A and B’s read operation B, even though A and B are performed in different threads, But the JMM assures the programmer that operation A will be visible to operation B). The specific definition is:

1) If an action happens before another, then the result of the first action will be visible to the second, and the first action will be executed before the second.

2) The existence of a happens-before relationship between two operations does not mean that specific implementations of the Java platform must be executed in the order specified by the happens-before relationship. If the result of the reorder is the same as the result of the happens-before relationship, then the reorder is not illegal (that is, the JMM allows it).

1) above is the JMM’s commitment to programmers. From the programmer’s point of view, the happens-before relationship can be understood this way: If A happens before B, then the Java memory model guarantees to the programmer that the result of an operation A will be visible to B, and that A is executed before B. Note that this is just what the Java memory model promises the programmer!

2) above is the JMM’s constraint on compiler and processor reordering. As mentioned earlier, the JMM really follows a basic principle: the compiler and processor can optimize as much as they like, as long as the execution result of the program (i.e., single-threaded programs and properly synchronized multi-threaded programs) is not changed. The reason why the JMM does this is that the programmer does not care whether the two operations are actually reordered. The programmer cares that the semantics of the program execution cannot be changed (that is, the result of execution cannot be changed). Thus, happens-before relationships are essentially the same thing as as-if-serial semantics.

Here’s a comparison of as-if-serial and happens-before:

as-if-serial VS happens-before

  1. The as-if-serial semantics guarantee that the execution result of a single-threaded program will not be changed, and the happens-before relationship guarantees that the execution result of a properly synchronized multithreaded program will not be changed.
  2. The as-if-serial semantics create a fantasy for programmers who write single-threaded programs: single-threaded programs are executed in program order. Happens-before relationships create a fantasy for programmers who write properly synchronized multithreaded programs: properly synchronized multithreaded programs execute in the order specified by happens-before.
  3. The purpose of as-if-serial semantics and happens-before is to improve the parallelism of program execution as much as possible without changing the execution result of the program.

4.2 Specific Rules

There are eight rules:

  1. Rule of program order: Every action in a thread happens before any subsequent action in that thread.
  2. Monitor lock rule: The unlocking of a lock happens before the subsequent locking of the lock.
  3. The volatile variable rule: a write to a volatile field happens before any subsequent reads to that field.
  4. Transitivity: If A happens-before B, and B happens-before C, then A happens-before C.
  5. Start () rule: if thread A performs the operation threadb.start () (which starts ThreadB), then thread A’s threadb.start () action happens-before any action in ThreadB.
  6. The join() rule: if thread A performs the operation threadb.join () and returns successfully, then any operation in ThreadB happens-before thread A returns successfully from the operation threadb.join ().
  7. Program interrupt rule: The call to the thread Interrupted () method precedes the interruption time detected by the interrupted thread’s code.
  8. The Object Finalize rule: The completion of an object’s initialization (completion of constructor execution) precedes the beginning of its Finalize () method.

Here’s a concrete example of how to use these rules to make inferences:

Again, the area of the circle is described as above. There are three happens-before relationships in the use of procedural order rules (Rule 1) : 1. A happens-before B; 2. 2. B happens-before C; 3. A) before B) before C) before D) after The third relation here is inferred using transitivity. A happens-before B. Definition 1 requires that the execution result of A is visible to B, and the execution order of A is before the execution order of B. However, at the same time, according to the second definition, there is no data dependence between A and B, and the execution order of the two operations has no impact on the final result. The happens-before relationship does not represent the final order of execution.

5. To summarize

Two aspects of the JMM have been discussed above: 1. The abstract structure of the JMM (main memory and thread working memory); 2. 2. Reordering and happens-before rules. Now, let’s make a conclusion. Think about it in two ways. 1. If we design the JMM, we should consider from what aspects, that is, what functions the JMM undertakes; Happens-before relationship with JMM; 3. 3. What problems can occur in multithreaded situations due to JMM?

5.1 JMM design

The JMM is a language-level memory model. In my understanding, the JMM is in the middle tier and contains two aspects :(1) the memory model; (2) Reordering and happens-before rules. At the same time, compiler and processor instruction sequences are controlled to prohibit certain types of reordering. On top of that, there will be jMM-based keywords and concrete classes under the J.U.C package to make it easier for programmers to program concurrently quickly and efficiently. From a JMM designer’s perspective, there are two key factors to consider when designing a JMM:

  1. Programmers want memory models to be easy to understand and easy to program. Programmers want to write code based on a strong memory model.
  2. Compiler and Processor implementations of memory Models Compilers and processors want the memory model to bind them as little as possible so that they can do as many optimizations as possible to improve performance. Compilers and processors want to implement a weak memory model.

Another particularly interesting thing about reordering is that, more simply, there are two types of reordering:

  1. A reorder that changes the results of program execution.
  2. A reorder that does not change the results of program execution.

The JMM has adopted different strategies for reordering these two different properties, as follows.

  1. The JMM requires compilers and processors to disallow reorders that change the results of program execution.
  2. The JMM does not require the compiler or processor to reorder programs that do not change the execution results. (The JMM allows such reordering.)

The design drawings of JMM are as follows:

As can be seen from the figure:

  1. The HAPPENs-before rules that the JMM provides to the programmer meet the programmer’s needs. The HAPPENs-before rule of the JMM is not only simple to understand, but also provides the programmer with A strong enough memory visibility guarantee (some memory visibility guarantees don’t really exist, such as A happens-before B above).
  2. The JMM already has as few constraints as possible on the compiler and processor. As you can see from the above analysis, the JMM is following a basic principle: the compiler and processor can optimize as much as they want, as long as the execution results of the program (i.e., single-threaded programs and properly synchronized multi-threaded programs) are not changed. For example, if the compiler, after careful analysis, determines that a lock will only be accessed by a single thread, the lock can be removed. For another example, if the compiler decides, after careful analysis, that a volatile variable should only be accessed by a single thread, the compiler can treat that volatile as a common variable. These optimizations will not change the execution result of the program, but also improve the execution efficiency of the program.

5.2 The relationship between happens-before and JMM

A happens-before rule corresponds to one or more compiler and handler reordering rules. Happens-before rules are straightforward for Java programmers, and they prevent Java programmers from having to learn complex reordering rules and how they are implemented in order to understand the memory visibility guarantees provided by the JMM

5.3 Issues that May Be Concerned in the Future

In addition, there are also some problems with reordering in multiple threads. For example, the classic problem is DCL (double-checked lock), which prohibits reordering. Atomic operations such as i++ under multiple threads are also prone to thread-safety problems without being noticed. But in general, atomicity, order and visibility should be considered in multithreaded development. The concurrency utility classes and concurrency containers under the J.U.C package also take time to master, and will be discussed more in future articles.

reference

The Art of Concurrent Programming in Java