Multi-threaded thread safety – javaee

Foreword

In this article, by understanding the causes of thread insecurity and the way to solve thread insecurity, there are generally problems with thread insecurity. If there are any mistakes, please correct them in the comment area. Let us communicate and make progress together!

Article directory

  • foreword
  • 1. Thread insecurity caused by multi-threaded execution
  • 2. Thread safety
      • Preemptive execution of thread <=> Root cause
      • Multiple threads modify the same variable
      • Thread modification is not atomic
      • Memory visibility problem
      • Command reordering problem
  • Summarize

This article begins

1. Thread insecurity caused by multi-threaded execution

Problem occurred:
In the case of multi-threading, the out-of-order scheduling of threads will produce bugs, which is called thread insecurity;

Reason:
Get acquainted with the example below!
Two threads calculate the value of the variable count together by calling the same method add of the same class. Each thread calls the method once to make the variable count self-increment once. Each thread calls the method add 10000 times, and the final result is 20000; but the result is indeed different from what we thought, let’s take a look through the code!
Two threads call the same method code implementation (has thread safety issues):

class Sum{
    private int count = 0;
    public void add() {
            count + + ;
    }
    public int getCount(){
        return count;
    }
}
public class ThreadDemo2 {
    public static void main(String[] args) throws InterruptedException {
        Sum s = new Sum();
        Thread t1 = new Thread( () -> {
            for (int i = 0; i < 10000; i ++ ) {
                s. add();
            }
        });
        Thread t2 = new Thread( () -> {
            for (int i = 0; i < 10000; i ++ ) {
                s. add();
            }
        });
        //Start the thread
        t1. start();
        t2.start();
        // thread waiting
        t1. join();
        t2. join();
        //Get calculation result
        System.out.println(s.getCount());
    }
}

The result is as follows:

The above result is only the result of one execution and can be executed multiple times. From the execution results, it can be found that the result of each execution will be less than 20000, which is the problem of thread safety;

What is the reason for the above result?
In fact, when the add method is called, the self-increment ++ operation is performed on the register in three parts: load, add, and save. The order of these three executions cannot be determined, so the register may be increased once, or Multiple times, but the final result only shows an increase of 1 time, and the resulting calculation result is less than 20000;

[Note] The essence of register + + operation:
load: read memory data into cpu registers
add: Perform + 1 operation on the value in the register = “increase by 1
save: Write the value in the register to the memory

It’s just that the language description may be a bit abstract, let’s understand it further by drawing pictures! ! !

Register complete self-increment operation

The above is to perform three operations strictly according to register 1 first, and then perform three operations on register 2, and the result is 2. If there is an intersection between the execution order of the three operations, thread safety may occur. Learn about the situation from the following figure ;

From the above figure, it can be seen that there is only one result of self-increment twice, and the result of one time is overwritten. This is only one of the cases. Many times, but the result is only a few times. This is because the multi-thread scheduling is out of order, so the execution order of these three steps is also uncertain, which leads to bugs, resulting in results that are less than 20000;

From this, let’s take a look at the common reasons for thread insecurity!

2. Thread safety

Why thread safety?
In the case of multi-threading, the out-of-order scheduling of threads will cause thread safety, also known as thread preemptive execution

The reason for thread safety:

Preemptive execution of threads <=> Root cause

Multiple threads execute operations, and the order of execution of operations cannot be determined, which is the preemptive execution of existing threads;

Multiple threads modify the same variable

Calculate a number, define a variable, count calculation, and use two or more threads to perform ++ operations on this count variable. Since the ++ operation is divided into three parts: load, add, and save, the order of execution of these three parts cannot be determined. As a result, one or two operations may be overwritten, resulting in a final result that is less than 20,000; this creates a bug;

The complete code refers to the initial code

Thread modification is not atomic

What is atomicity?
Atom: the smallest unit that cannot be divided, treat some operations as a whole and cannot be separated;
For example: ++ operation, its corresponding CPU instruction can be regarded as three parts, load, add, save, if executed separately, it is considered not atomic; these three parts must be regarded as a whole, and then executed can be regarded as an atomic operation;

The question is coming again, how to make the thread atomic? = “Lock
Know the two operations of the lock:
① Locking: When a thread is locked, other threads must wait for the execution of this thread to end
② Unlock: After the thread is unlocked, other threads can continue to compete for the lock;

The locking operation needs to use the keyword: synchronized;
Use keywords to modify the code block, and put the code that needs to be locked in the thread into the code block, which realizes atomicity;

The purpose of the lock operation:
Locking is to serialize part of the code of the two threads, and most of the code is concurrent;
This needs to be distinguished from join, which allows two threads to be serialized completely, not partially;
Partial serialization example: two threads of the above code call a method add, before adding, the loop variable i will be created, and the loop condition will be judged. After calling add, count will + +, and after locking count , the operation before count is considered to be concurrent. After thread 1 calls count and executes, thread 2 calls count again, which is serialized. The execution after count returns, and operations such as variable i ++ are also concurrent;

Modify the above 2 codes and lock the count: the final count result is 20000

class Sum{
    private int count = 0;
    public void add() {
        synchronized (this) {
            count + + ;
        }
    }
    public int getCount(){
        return count;
    }
}

Different locking methods:

In the lock operation () brackets: inside is the lock object, which cannot be the basic data type;
Static class lock () is a class object in parentheses, as shown above;
[Note] Class object: Indicates the content of the .class file (methods, attributes, etc.)

Memory visibility problem

Through a piece of code, the memory visibility problem is found:

 public static int flag = 0;//Control loop condition
    public static void main(String[] args) {
        Thread t = new Thread( () -> {
            while (flag == 0) {

            }
            System.out.println("End of cycle!");
        });
        Thread t2 = new Thread( () -> {
            Scanner scanner = new Scanner(System.in);
            System.out.println("Enter an integer:");
            flag = scanner. nextInt();
        });
        t. start();
        t2.start();
    }

The above code is originally, the t thread execution loop is an infinite loop, wait for the t2 thread to perform the input operation to change the value of the flag, so that flag!=0, the condition is the end of the false loop; but the loop does not end, the reason is actually the memory visibility problem ;

Judging while(falg == 0) This conditional instruction has two steps
1.load: read data from memory to cup register
2.cmp: Whether the value in the comparison register is 0
[Note] Reading speed: register > memory > hard disk
Generation of memory visibility issues:
According to the reading speed, it is found that load reads memory with a large overhead, because it is an infinite loop, and the result is the same when the load is not modified, the compiler will make optimization operations to optimize the load; in this way, only the first time the load is executed , the latter operation only performs the cmp comparison operation;
In this way, the t2 thread changes the value of the flag, but the register is no longer read, and only the value before the modification is used, the operation will be in an endless loop, and thread safety issues will occur;

Memory Visibility: In a multi-threaded environment, the compiler optimizes the code, resulting in a misjudgment (like the above code that the value of the flag has not been changed), which causes a bug and leads to a code error;
[Note] Compiler optimization: Intelligently adjust the code execution logic. Under the premise of ensuring that the program result remains unchanged, through a series of operations such as adding and subtracting statements, statement transformation, etc., the code execution efficiency is improved;
Dealing with memory visibility issues:
Use the volatile keyword: for variables modified by volatile, the compiler will prohibit code optimization, so as to ensure that the data is re-read from memory every time;
Code modification:

 volatile public static int flag = 0;//Control loop condition
  //volatile: guarantee memory visibility

[Note] volatile: 1. Atomicity is not guaranteed, applicable to scenarios where one thread reads and one thread writes; synchronized: multiple threads write;
2.volatile prohibits instruction reordering

Command reordering problem

Problem: Due to the different order of execution of some code operations, bugs may occur;
Instruction reordering: Compiler optimization, to ensure that the overall logic remains unchanged, adjust the execution order of the code, and make the program more efficient;

For example, the new object operation is considered to be divided into 3 steps:
1. First apply for memory space
2. Call the constructor (initialize memory data)
3. Object reference assignment (assignment of memory address)
Thread 1 executes operations 1 and 3 first, and the execution order of operations 2 is uncertain. During this period, other threads use the object and call its method attributes. Although the object is not empty, but it is not initialized, bugs may occur;
Solution: Add volatile to the code, and the created one will prohibit instruction reordering;

Summary

?Dear readers, if the content shared in this article is helpful to you, please give it a like and encouragement! !
Thanks to every partner who came here together, we can communicate and make progress together! ! ! let’s work hard together! ! !