Abstract: IN order to optimize the program, THE CPU will reorder the instructions of the program. At this time, the execution order of the program is not necessarily consistent with the writing order of the code, which may cause the ordering problem.

This article is shared from Huawei cloud community “[High Concurrency] Decryption caused by the third behind the concurrency problem – Order problem”, author: Glacier.

order

Orderliness refers to executing code in a given order.

In layman’s terms, code is executed in a specified order, for example, in the order in which a program is written, the first line of code is executed, then the second, then the third, and so on. As shown in the figure below.

Instruction reordering

The compiler or interpreter sometimes changes the order in which a program is executed to optimize its performance. However, compiler or interpreter changes to the order in which the program is executed can cause unexpected problems!

In a single thread, instruction reordering ensures that the result of the final execution is the same as the result of the sequential execution of the program, but in multithreading, there are problems.

If an instruction reorder occurs, the program might execute the first line of code, then the third, then the second, as shown below.

Take the following three lines of code.

int x = 1; 
int y = 2;
int z = x + y;
Copy the code

When the CPU reorders instructions, it can ensure that x=1 and y = 2 are at the top of z = x+ y, but the order of x=1 and y = 2 is not necessarily the same. This is not a problem with single threads, but not with multiple threads.

Order problem

In order to optimize the program, the CPU will reorder the instructions of the program. In this case, the execution order of the program is not necessarily the same as the order in which the code was written, which may cause ordering problems.

A classic example in Java programs is the use of double-checking to create singleton objects. For example, in the following code, when getting an instance of an object in the getInstance() method, it first determines whether the instance object is empty. If so, it locks the class object of the current class and again checks whether instance is empty. If instance is still empty, Creates an instance of the instance object.

package io.binghe.concurrent.lab01; /** * @author binghe * @version 1.0.0 * @description test singleton */ public class SingleInstance {private static SingleInstance  instance; public static SingleInstance getInstance(){ if(instance == null){ synchronized (SingleInstance.class){ if(instance == null){ instance = new SingleInstance(); } } } return instance; }}Copy the code

If the compiler or interpreter does not optimize the above program, the entire code is executed as follows.

Note: To give you A clearer idea of the order in which the flow diagram is executed, I have numbered it to specify the order in which thread A and thread B execute.

If thread A and thread B both call the getInstance() method to obtain an instance of the singleInstance.class, both threads will find the instance object empty and lock the singleInstance.class. The JVM guarantees that only one thread will acquire the lock. Here we assume that thread A has acquired the lock. Thread B waits because it has not acquired the lock. Next, thread A again determines that the instance object is empty, creates an instance of the instance object, and finally releases the lock. At this point, thread B wakes up and tries to acquire the lock again. After obtaining the lock successfully, thread B checks that the instance object is no longer empty and thread B does not create the instance object.

All of this looks perfect, but only if the compiler or interpreter doesn’t optimize the program, which means the CPU doesn’t reorder the program. But in reality, it’s all just us thinking it is.

When you run the above code to get an instance object in a truly high-concurrency environment, the creation of the object’s new operation is problematic because of compiler or interpreter optimization of the program. In other words, the root of the problem is one line of code.

instance = new SingleInstance();
Copy the code

For the above line of code, three CPU instructions would correspond to it.

1. Allocate memory space.

2. Initialize the object.

3. Point the instance reference to memory space.

Normally, CPU instructions are executed in the order of 1 – >2 – >3. After the CPU reorders the program, the execution order may be 1 – >3 – >2. This is where problems arise.

When the CPU reorders the program in A sequence of 1 – >3 – >2, we summarize the two steps of thread A and thread B calling the getInstance() method to get the object instance as follows.

[First step]

(1) Assume that thread A and thread B enter the first if condition at the same time.

(2) Assume that thread A first obtains the synchronized lock and enters the synchronized code block. At this time, because the instance object is null, instance = new SingleInstance() statement is executed.

(3) When instance = new SingleInstance() is executed, thread A creates an empty space in the JVM.

(4) Thread A points the instance reference to the blank memory space. When the object is not initialized, A thread switch occurs. Thread A releases the synchronized lock, and the CPU switches to thread B.

(5) Thread B enters the synchronized code block and reads the instance object returned by thread A. At this time, the instance object is not null, but the object initialization operation has not been carried out, and it is an empty object. If thread B uses instance, it may have a problem!!

[Second step]

(1) Thread A enters if condition judgment first,

(2) Thread A acquires synchronized lock and performs the second if condition judgment. At this time, instance is null and the instance = new SingleInstance() statement is executed.

(3) Thread A creates an empty space in the JVM.

(4) Thread A points the instance reference to the empty memory space. When there is no object initialization, A thread switch occurs, and the CPU is switched to thread B.

(5) Thread B conducts the first if judgment and finds that the instance object is not null, but the instance object has not been initialized and is an empty object. If thread B uses the instance object directly, there may be a problem!!

In the second step, even if thread A does not release the lock when thread switch occurs, thread B finds instance is not null when it makes its first if judgment and directly returns instance without attempting to acquire synchronized lock.

We can simplify the above process as shown below.

conclusion

There are three root causes of all kinds of weird problems in concurrent programming: visibility problems caused by caching, atomicity problems caused by thread switching, and orderliness problems caused by compilation optimizations. Understanding the root causes of these three problems can help us better write highly concurrent programs.

Click to follow, the first time to learn about Huawei cloud fresh technology ~