How JAVA solves the visibility and order-java memory model (Happens-Before rule, volatile, synchronized, final)


Everyone knows the three root problems of java concurrency: visibility, order, and atomicity. So how does java solve it?

Here is how JAVA solves the problems of visibility and order.

The reason for visibility is caching, and the reason for ordering is compilation optimization. Then we only need to disable caching and compilation optimization as needed.

JAVA launched the JAVA memory model. The JAVA memory model specifies that the JVM provides methods for disabling caching and compilation optimization on demand. Specifically, there are three keywords : volatile, synchronized, and final , and the Happens-Before rule .

Happens-Before rule

happens-before仅仅要求前一个操作的执行结果对后一个操作是可见的,且前一个操作按顺序排在后一个操作之前。

Happens-Before constrains the optimization behavior of the compiler. Although compiler optimization is allowed, it requires the compiler to abide by the Happens-Before rules after optimization. Let me introduce these 8 rules: A operation happens-before B operation == A happens B before

  1. Program sequence rules: each operation in a thread, happens-before any subsequent operation in the thread.
  2. Monitor lock rules: For unlocking a lock, happens-before locking the lock later.
  3. Volatie variable rule: For a write operation of a volatile variable, happens-before subsequent read operation of the variable.
  4. Transitivity rules: If happen-beford B, and B happen-before C, then A happen-before C;
  5. Thread start () rule: After main thread starts child thread B, child thread B can see the operation before main thread starts child thread B.
  6. Thread join () rule: Thread A calls thread B's join () method. After thread A waits for thread B to execute join () and returns successfully, any operation of thread B is visible after thread B.join ().
  7. Thread interruption rules: The call to the thread interrupt () method occurs first when the interrupted thread code detects the occurrence of an interruption event, and you can detect whether an interruption occurs through Thread.interrupted ().
  8. Object finalization rule: This is also simple, that is, the completion of the initialization of an object, that is, the end of the execution of the constructor must happen-before its finalize () method.

The following are three keywords: volatile, synchronized, and final.

volatile keyword

Volatile can guarantee visibility and order.

  • How does volatile ensure order

Reordering is divided into compiler reordering and processor reordering. In order to achieve volatile memory semantics, JMM can not reorder the following cases in order to limit these two reorderings!

1.当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序;
2.当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序;
3.当第一个操作是volatile写,第二个操作是volatile读,不能重排序;

In order to implement the above rules (volatile memory semantics), when the compiler generates bytecode, it will insert a memory barrier in the instruction sequence to prohibit processor reordering. But for the compiler, it is impossible to find an optimal layout to minimize the number of memory barriers inserted, so JMM inserts different memory barriers before and after each volatile read and write to achieve ordering.

在每个volatile写操作的前面插入一个StoreStore屏障。
在每个volatile写操作的前面插入一个StoreLoad屏障。
在每个volatile读操作的前面插入一个LoadLoad屏障。
在每个volatile读操作的前面插入一个LoadStore屏障。

If you want to understand the role of the four barriers, you can check the information yourself.

  • How volatile guarantees visibility

When a shared variable modified with volatile is written, the assembly code of the lock will be more, and the instruction with the lock prefix will perform two operations under the multi-core processor.

1.将当前缓存行中的数据写回到系统内存中。
LOCK#信号会锁定这块内存区域的缓存,并写回内存,并使用缓存一致性,保证它的原子性。
2.这个写回内存的操作会使其他cup里缓存了该内存地址的缓存行无效。

Refer to the principle of volatile for details

final keyword

For the reordering rules of the final field, the compiler and the processor must follow the following two rules:
First, show these two situations with java code.

public class FinalDemo{
int i;//普通变量
final int j;//final变量
static FinalDemo demo;
public FinalDemo(){//构造函数
	i=1;//写普通域
	j=2;//写final域
	}
public static void writer(){//写线程A执行
demo=new FinalDemo();
}
public static void reader(){//读线程B执行
FilalDemo object=demo;//读对象引用
int a=object.i;//读普通域
int b=object.j;//读final域
}
}
  1. A writes the final field in the constructor, and B subsequently refers the constructed object to a reference variable. These two operations cannot be reordered.
    Insert picture description here

  2. Reading a reference to an object containing a final field for the first time, and subsequently reading the final field for the first time, these two operations cannot be reordered.
    Insert picture description here
    The final field we saw above is the basic data type, so what if the final field is a reference type?
    For reference types, the reordering rules for writing final fields impose the following constraints on the compiler and processor.

  3. The writing of a member field of a final reference object in the constructor, and the subsequent reading of the member field of the final field reference object outside the constructor, these two operations cannot be reordered.
    Please see the example code below.

public class FinalReferenceDemo {
	final int[] arrays; //final是引用类型
	static FinalReferenceDemo demo;
	public FinalReferenceDemo() {//构造函数
		arrays = new int[1];//1
		arrays[0]=1;//2
	}
	public static void writeOne(){//写线程A执行
		demo=new FinalReferenceDemo();//3
	}
	public static void writeTwo(){//写线程B执行
		demo.arrays[0]=2;//4
	}
	public static void reader(){//读线程C执行
		if(demo!=null){
			int temp=demo.arrays[0];
		}
	}
}

Insert picture description here

  1. Constructor overflow problem
    Look at the following code
public class FinalReferenceEscapeDemo {
	final int i;
	static FinalReferenceEscapeDemo obj;
	public FinalReferenceEscapeDemo() {// 构造函数
		i = 1;
		obj = this;
	}
	public static void writer() {
		new FinalReferenceEscapeDemo();
	}
	public static void reader() {// 读线程C执行
		if (obj != null) {
			int temp = obj.i;
		}
	}
}

Constructor escape
If the above example is too complicated, you can understand it in conjunction with the following example.
We usually have three steps when we create a new object.
We imagine this is the case.

  1. Allocate a memory M;
  2. Initialize the Singleton object on memory M;
  3. Then the address of M is assigned to the instance variable.

In fact, the compiler is optimized like this

  1. Allocate a memory M;
  2. Then the address of M is assigned to the instance variable;
  3. Initialize the Singleton object on memory M;

In this way, after step 2, the instance actually has a reference to memory M, but the value is not initialized, so that the object can be obtained in another thread, but it is not initialized, such as int i in the object; i does not Initialization, the call will error.

synchronized keyword

As mentioned in other articles, I won't go into details here.

Reference book: The Art of Java Concurrent Programming

Published 34 original articles · Likes0 · Visits 1089

Guess you like

Origin blog.csdn.net/qq_42634696/article/details/104880010