Thread safety was mentioned in the String, StringBuiler, and StringBuffer interviews, so today we’ll talk about the source of concurrency problems.

Atomicity problems caused by thread switching

Either one or more operations are performed in full and without interruption by any factor, or none of them are performed at all.

A classic example is bank account transfers:

For example, transferring $1000 from account A to account B must involve two operations: subtracting $1000 from account A and adding $1000 to account B. These two operations must be atomic to ensure that there are no unexpected problems.

The same is true when we manipulate data, such as I = I +1; It’s reading the value of I, calculating I, writing I. This line of code is not atomic in Java.

Visibility issues due to CPU cache

When multiple threads access the same variable, one thread modifies the value of the variable, and the other threads immediately see the changed value.

If the two threads are on different cpus, thread A and thread B execute simultaneously, and the shared variable V is cached from main memory into local memory. Thread A has modified the shared variable V, which has not yet been written to main memory, and thread B has also modified the variable V, which is invisible to each other. So when they flush the value of V into main memory, one thread must override the other thread’s result. This is visibility.

Order problems caused by compilation optimization

The order in which a program is executed is the order in which the code is executed.

Generally speaking, in order to improve the efficiency of the program, the processor may optimize the input code. It does not guarantee that the execution sequence of each statement in the program is the same as that in the code, but it will ensure that the final execution result of the program is the same as that in the sequence of the code execution. As follows:

int a = 10;    //语句1
int r = 2;    //语句2
a = a + 3;    //语句3
r = a*a;     //语句4 
Copy the code

Because of the reordering, he could have executed the order 2-1-3-4, 1-3-2-4, but never 2-1-4-3, because that breaks the dependency. Obviously, reordering is not a problem for single-threaded applications, but not for multithreaded applications, so we need to consider this when we’re programming multithreaded applications. Ex. :

    int a = 0;
    boolean flag = false;

    public void write(a) {
        a = 1;  / / 1
        flag = true; / / 2
    }

    public void read(a) {
        if (flag) { / / 3
            int b = a * a; / / 4}}Copy the code

In the preceding code, thread A executes write() and thread B executes read(). If flag=true is the first instruction to be reordered, then thread B switches to execute, and the result is incorrect.

Double check locking mechanism is thread safe

public class DoubleCheckLockSingleton {


    private static DoubleCheckLockSingleton INSTANCE = null;

    public static DoubleCheckLockSingleton getINSTANCE(a){
        if (INSTANCE == null) {  // The first check
            synchronized (DoubleCheckLockSingleton.class) {
                if (INSTANCE == null) { // The second check
                    INSTANCE = newDoubleCheckLockSingleton(); }}}return INSTANCE;
    }

    private DoubleCheckLockSingleton(a) {}}Copy the code

Looking at the code above, many people can see at first glance that INSTANCE is missing the volatile keyword. Do DCL singletons need volatile at all?

Using above to explain, thread 1 is executing instance initialization, but because the object initialization operation instruction is not atomic operation, so the execution of initialization and pointed to the memory address space allocation good object two instructions in the event of instruction on rearrangement, so for other threads when not visible, when astore instruction execution is completed, If instance is no longer null, thread 2 will return an uninitialized instance.

conclusion

Atomicity, visibility, and order all lead to concurrency problems, and all concurrency problems revolve around these three characteristics.