Java concurrent programming combat (2)-Java memory model

Java concurrent programming combat (2)-Java memory model

This article mainly describes the Java memory model and Happens-Before rules that are very important in Java concurrent programming.

Overview

There are three main causes for the various problems of Java concurrent programs:

  • Visibility issues caused by caching
  • Atomicity caused by thread switching
  • Order problems caused by compilation optimization

In order to solve the problem of visibility and order, Java introduced the Java memory model. We will introduce it in this article.

Visibility and ordering problems are caused by caching and compilation optimization. The most direct way is to disable caching and compilation optimization. This can solve the problem, but the performance of the program will drop to an unacceptable level.

A reasonable solution is to disable caching and compilation optimization on demand. The so-called "disable on demand" means to disable it according to the requirements of the programmer to open up corresponding methods to the programmer.

What is the Java memory model

The Java memory model is a very complex specification that can be interpreted from different angles. From the perspective of the programmer, it can be solved as it regulates how the JVM provides on-demand caching and compilation optimization methods.

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

The difference between Java memory model and JVM

  • The Java memory model defines a set of specifications that allow the JVM to disable CPU caching and compilation optimization on demand. This set of specifications includes three keywords: volatile, synchronized, and final, and seven Happen-Before rules.
  • The JVM memory model refers to the five parts of the program counter, JVM method stack, local method stack, heap, and method area.

volatile keyword

The purpose of the volatile keyword is to disable the CPU cache.

For example, we define a volatile variable volatile int x = 0;, which expresses: When the compiler reads and writes this variable, it cannot use the CPU cache, but directly operates from the 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));
		}
	}
}
 

If there are two threads for the same VolatileDemo object, one calls the write() method and the other calls the read() method, then when v is true in the read() method, what is the value of x?

Before Java 1.5, the value of x may be 0 or 42, after Java 1.5, the value of x can only be 42.

This is due to the Happens-Before rule.

Happens-Before rules

What is the Happens-Before rule?

The Happens-Before rule expresses that the result of the previous operation is visible to subsequent operations. It constrains the optimization behavior of the compiler and ensures that it must comply with the Happens-Before rule.

The semantic essence of Happens-Before is a kind of visibility. A Happens-Before B means that the A event is visible to the B event, regardless of whether the A event and the B event occur in the same thread.

There are many Happens-Before rules, of which 6 are related to programmers. Let's describe them one by one.

Sequential rules

In a thread, in accordance with the program sequence, the previous operation Happens-Before any subsequent operations.

This rule is relatively intuitive and conforms to the thinking in single thread: the modification of a certain variable before the program must be visible to subsequent operations.

Volatile variable rules

For a write operation of a volatile variable, Happens-Before is followed by a read operation of this volatile variable.

Transitivity

If A Happens-Before B, and B Happens-Before C, then A Happens-Before C.

Let's look at the example code above:

  • x=42 Happens-Before write variable v=true, this is rule 1.
  • Write variable v=true Happens-Before Read variable v=true, this is rule 2.

Then according to the transitivity rule, we can get x=42 Happens-Before read variable v=true . So in the sample code, when v==true is judged, the value of x is equal to 42.

synchronized rule

Unlocking a lock requires Happens-Before to lock the lock later.

We must first understand what a "monitor" is. A monitor is an important concept in the operating system. A monitor is a collection of processes, variables, and data structures. It consists of four parts: 1) Program name, 2) description of shared data, 3) a set of procedures for operating on data, 4) statement that assigns initial values to shared data.

In Java, monitors are implemented through the synchronized keyword.

We can understand this rule as follows: Suppose that the initial value of x is 10, thread A acquires the lock, after executing the code, the value of x will become 12, then the lock is released, and then thread B acquires the lock, and then thread B sees The x must be 12, it should not be 10.

Thread start() rule

The main thread A starts the child thread B, and the child thread B can see the operation of the main thread before starting the 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();
		
	}
}
 

The output of the program is as follows.

x is 20.
 

Thread join() rules

The main thread A waits for the end of the child thread by calling the join() method of the child thread. When the child thread ends, the main thread can see the operation of the child thread on the shared variable.

This rule is similar to the thread start() rule, let s look at the following sample 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();
		
	}
}
 

The output of the program is as follows.

x is 30.
 

final rule

When we use final to modify a variable, we tell the compiler that the variable is unchanged and can be optimized.

But if we set the variable to final, its constructor may still cause errors due to the error rearrangement after compilation and optimization, such as the singleton code we talked about earlier.

After Java 1.5, the Java memory model restricts the rearrangement of final type variables. As long as the constructor provided by us does not "escape", there will be no problem.

The so-called "escape" refers to the use of variables whose life cycle exceeds the object life cycle in the constructor.

Reference