This article focuses on the Java memory model and happens-before rules, which are important in Java concurrent programming.

An overview of the

There are three main causes of the various problems with Java concurrent program issues:

  • Visibility issues caused by caching
  • Atomicity problems caused by thread switching
  • Orderliness issues caused by compiler optimizations

To address visibility and order issues, Java introduced the Java Memory model, which we introduce in this article.

Visibility and orderliness problems are caused by caching and compilation optimizations, so the most straightforward approach is to disable caching and compilation optimizations, which will solve the problem, but the performance of the program will drop to an unacceptable level.

The logical solution is to disable caching and compilation optimizations on demand. “Disable on demand” means to disable caching and compilation optimizations at the programmer’s request, leaving the corresponding methods open to the programmer.

What is the Java Memory model

The Java Memory model is a complex specification that can be interpreted from different perspectives, and it can be addressed from a programmer’s point of view as it specifies how the JVM provides ways to disable caching and compile optimizations on demand.

The corresponding specification for the Java memory model is JSR-133, linked to www.cs.umd.edu/~pugh/java/… .

Differences between the Java memory model and the JVM

  • The Java memory model defines a set of specifications that enable JVMS to disable CPU caching and compilation optimizations on demand, including the keywords volatile, synchronized, and final, and seven Happen-Before rules.
  • The JVM memory model refers to the program counter, JVM method stack, local method stack, heap, and method area.

The volatile keyword

The use of the volatile keyword is to disable CPU caching.

For example, we define a volatile variable volatile int x = 0; The compiler does not use the CPU cache when reading or writing to this variable. Instead, it reads or writes from memory.

Let’s look at the following code example.

public class VolatileDemo { int x = 0; volatile boolean v = false; public void write() { x = 42; v = true; } public void read() { if (v == true) { System.out.println(String.format("x is %s", x)); }}}Copy the code

If the same VolatileDemo object has two threads, one calling write() and the other read(), what is the value of x when v equals true in read()?

Before Java 1.5, the value of x could be 0 or 42. After Java 1.5, the value of x could only be 42.

This is due to the happens-before rule.

Happens-before rules

What is the happens-before rule?

The happens-before rule states that the result of a previous action is visible to subsequent actions. It construes the compiler’s optimization behavior to follow the happens-before rule.

The semantic nature of happens-before is visibility. A happens-before B means that event A is visible to event B, regardless of whether event A and event B occur in the same thread.

There are many happens-before rules, of which six are relevant to programmers, and we’ll describe them all.

Sequential rule

In a thread, the previous action is happens-before any subsequent action, in procedural order.

This rule is intuitive and conforms to the single-thread mentality: changes made to a variable in front of the program must be visible to subsequent operations.

Volatile variable rule

A write to a volatile variable is happens-before a subsequent read to that volatile variable.

transitivity

If A happens-before B, and B happens-before C, then A happens-before C.

Let’s look at the sample code above:

  • X =42 happens-before write the variable v=true, that’s rule 1.
  • Write variable v=true happens-before read variable v=true, that’s rule 2.

Then by the transitivity rule, we can say that x=42 happens-before reads the variable v=true. So in the sample code, the value of x is 42 when v==true is determined.

Synchronized rules

A lock is unlocked happens-before it is unlocked later.

We must first understand what is “monitor”, tube side is an important concept in the operating system, a monitor is a process, variables, and a collection of data structure, it consists of four parts: 1) the name passes, 2) to share data, 3) the data is a set of process operation, 4) to Shared data initialization statement.

In Java, pipe procedures are implemented with the synchronized keyword.

We can interpret this rule as follows: suppose x starts at 10, thread A acquires the lock, executes the code, x changes to 12, releases the lock, and then thread B acquires the lock, thread B must see x as 12, not 10.

Thread start() rule

The main thread A starts child thread B, and child thread B can see what the main thread did before it started child thread B.

Let’s look at the following example.

public class HappensBeforeDemo { private int x = 10; public void threadStartTest() { Thread t = new Thread(() -> { System.out.println(String.format("x is %s.",x)); }); x = 20; t.start(); } public static void main(String[] args) { HappensBeforeDemo demoObj = new HappensBeforeDemo(); demoObj.threadStartTest(); }}Copy the code

The output of the program is as follows.

x is 20.
Copy the code

Thread join() rule

The main thread A waits for the child thread to terminate by calling its join() method. When the child thread terminates, the main thread can see what the child thread does to the shared variable.

This rule is similar to the thread start() rule, so let’s look at the following example code.

public class HappensBeforeDemo { private int x = 10; public void threadJoinTest() throws InterruptedException { Thread t = new Thread(() -> { try { java.lang.Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } x = 30; }); t.start(); t.join(); System.out.println(String.format("x is %s.",x)); } public static void main(String[] args) throws InterruptedException { HappensBeforeDemo demoObj = new HappensBeforeDemo(); demoObj.threadJoinTest(); }}Copy the code

The output of the program is as follows.

x is 30.
Copy the code

The final rule

When we modify a variable with final, we tell the compiler that the variable is immutable and can be optimized as much as we want.

However, if we make the variable final, its constructor may still cause errors due to compile-optimized error rearrangement, such as the singleton code we talked about earlier.

After Java 1.5, the Java memory model imposed constraints on the rearrangement of variables of final type, and as long as the constructors we provided didn’t “escape,” then we wouldn’t be a problem.

By “escape,” we mean that a constructor uses a variable whose life span exceeds that of the object.

The resources

  • Time.geekbang.org/column/arti…
  • www.cs.umd.edu/~pugh/java/…
  • www.cs.umd.edu/~pugh/java/…
  • Docs.oracle.com/javase/spec…
  • Blog.csdn.net/javazejian/…