Summarize thread safety issues and their solutions

1. Thread safety (emphasis)

Thread unsafe: Under random scheduling, there are many possibilities for program execution, some of which may cause bugs in the code

A typical example: Two threads perform concurrent self-increment on the same variable, and the result of the operation at this time is different from the expected one.

// 创建两个线程, 让这俩线程同时并发的对一个变量, 自增 5w 次. 最终预期能够一共自增 10w 次.
class Counter {
    
    
    // 用来保存计数的变量
    public int count;

     public void increase() {
    
    
        count++;
    }

}
public class Demo13 {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        Counter counter=new Counter();
        Thread t1=new Thread(()->{
    
    
            for(int i=0;i<50000;i++){
    
    
                counter.increase();
            }
        });
        t1.start();
        Thread t2=new Thread(()->{
    
    
            for(int i=0;i<50000;i++){
    
    
                counter.increase();
            }
        });

        t2.start();
          try {
    
    
            t1.join();
            t2.join();//保证t1,t2先执行完
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        System.out.println("count:"+counter.count);

    }
}

Threads are not safe, and the order of random scheduling is different, which leads to different running results of the program

insert image description here

Like count++, one line of code corresponds to three machine instructions

1) Read data from memory to CPU (load)

2) In the CPU register, complete the addition operation (add)

3) Write the data of the register back to the memory (save)

insert image description here

The situation listed above is just one of them

Due to the scheduling of the scheduler, the final result of count is uncertain


Causes of thread insecurity :

  1. Random scheduling/preemptive execution of the operating system

  2. Multiple threads modify the same variable (when writing code, you can control this point, and you can control it by adjusting the program design. This method has a limited scope of application, and not all scenarios can be avoided)

  3. Some modification operations are not atomic (for example, ++, ++ corresponds to three machine instructions, it is not atomic) (operations can be packaged into atoms by locking operations)

  4. Memory visibility, thread safety issues caused by

For example, thread 1 repeatedly reads and judges. If under normal circumstances, thread 1 is reading and judging, it is normal for thread 2 to write suddenly. After thread 2 finishes writing, thread 1 can immediately read the memory changes. So that the judgment changes, but during the running of the program, an operation "optimization" may be involved (this "optimization" may be performed by the compiler javac, the JVM, or the operating system)

insert image description here

For example, thread 1 needs to repeatedly load, that is, read operations. Before thread 2 modifies, thread 1 reads repeatedly, and the data read each time is the same. JVM has made such an "optimization" and will not repeat it. Read from the memory, just reuse the data read from the memory to the register for the first time (because reading from the register is fast), after optimization at this time, thread 2 suddenly writes a data, because thread 1 It has been optimized to be read as a register, so the modification of thread 2 will not be perceived by thread 1.

Memory visibility problem : The memory has been changed, but in the context of optimization, it cannot be read or seen

In response to this problem, Java introduced the volatile keyword, allowing programmers to manually prohibit the compiler from performing the above optimization on a variable

  1. Instruction reordering may also cause thread unsafety

Instruction reordering is also an operating system/compiler/JVM optimization operation

For example: statement 1, statement 2, statement 3, instruction reordering is to adjust the order here to speed up the effect

For example: Xiao Ming goes shopping for vegetables and wants to buy 1. Tomatoes, 2. Eggs, 3. Eggplants, 4. Small celery, but the placement of vegetables in the supermarket is 1. Eggplants, 2. Tomatoes, 3. Eggs, 4. Small celery; so we adjust the order and buy eggplants, tomatoes, eggs, and small celery first, so that the efficiency will be high

No matter how optimization is performed, the premise is to ensure that the logic of the program remains unchanged

1) If it is single-threaded, it is easy to ensure that the logic remains unchanged

2) Under multi-threading, it is not easy to ensure that the logic remains unchanged

For example: Test t=new Test();

There are three steps to create such an object 1. Create a memory space 2. Construct an object on this memory space 3. Assign the reference of this memory to t;

At this time, another thread tries to read the reference of t. If it is 2,3, when the second thread reads that t is non-null, t must be a valid object at this time; if according to 3,2, the second thread When a thread reads that t is non-null, it may still be an invalid object.

Here, the problem of instruction reordering is easy to appear. The order of 2 and 3 can be exchanged. Under single thread, it will not affect the order of these two.

Solve atomicity (lock operation)

One of the reasons for thread insecurity is that the modification operation is not atomic, but we can pack the modification operation into an "atomic" operation in three instructions by adding a lock operation.

synchronized : The synchronized here is literally translated as "synchronization", and the "synchronization" here refers to "mutual exclusion"

In addition to synchronization, there are also usages of "synchronous" and "asynchronous" (IO scenarios or scenarios of upper-lower-level calls)

Example: I went to a restaurant for dinner and ordered egg fried rice

Synchronization: The caller is responsible for the obtained call results (after I initiate the request, I just stare at the bar and wait for the kitchen to make the meal, and I will take it away directly)

Asynchronous: This means that the caller is not responsible for obtaining the call result, and the caller will actively push the calculated result (after I initiate the request, I don’t care about it, I will find a place to play with the mobile phone, and wait for the kitchen to do it) Well, a waiter brought it to me)

insert image description here

class Counter {
    
    
    // 用来保存计数的变量
    public int count;

   synronized  public void increase() {
    
    //与上面不同的是加锁了
        count++;
    }

}
public class Demo13 {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        Counter counter=new Counter();
        Thread t1=new Thread(()->{
    
    
            for(int i=0;i<50000;i++){
    
    
                counter.increase();
            }
        });
        t1.start();
        Thread t2=new Thread(()->{
    
    
            for(int i=0;i<50000;i++){
    
    
                counter.increase();
            }
        });

        t2.start();
          try {
    
    
            t1.join();
            t2.join();//保证t1,t2先执行完
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        System.out.println("count:"+counter.count);
		
    }
}

.Resolve memory visibility (volatile keyword)

Memory visibility is also one of the reasons for thread insecurity

The scenario for memory visibility is: one thread writes, one thread reads

public class Demo15 {
    
    
    public static int test = 0;  //定义一个整形变量

    public static void main(String[] args) {
    
    
        Thread t1 = new Thread(() -> {
    
    
            while (test == 0) {
    
    
                //啥都不干
            }
            System.out.println("线程1执行完了");  //循环结束, 则打印这个语句
        });

        Thread t2 = new Thread(() -> {
    
    
            Scanner in = new Scanner(System.in);
            System.out.println("输入 ->"); //在线程2中偷偷改变 test 的值
            test = in.nextInt();
        });
        t1.start();
        t2.start();

        try {
    
    
            t1.join(); //线程等待
            t2.join();
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }

}

The expected effect is that the t2 thread enters a non-zero number here, at this time the t1 thread loop ends, and the process ends

The actual phenomenon is: when we enter a non-zero number, the t1 thread does not end

reason:

  • The work done by t1, LOAD: read the data in the memory to the CPU, TEST: check whether the value of the CPU register is the same as expected;
  • The work of t1 is repeated many times. Because reading memory is thousands of times slower than reading CPU registers, tens of thousands of times, it means that the current t1 operation is mainly slow on LOAD. After compiling, the result of each LOAD is nothing. Changes are directly "optimized",
  • That is equivalent to only reading data from the memory once, and then performing repeated TEST directly from the register.

So if you want to solve the memory visibility problem that appeared above, you can add the volatile keyword, that is, change public static int test=0; to

public static volatile int test=0;

The volatile operation is equivalent to explicitly prohibiting the compiler from performing the above optimization. It adds a "memory barrier" (special binary instruction) to the corresponding variable. When the JVM reads this variable, because of the existence of the memory barrier, Just know that you have to re-read the contents of this memory every time, instead of sloppy optimization

Summarize the role of volatile :

  • A memory barrier is added to this variable by special binary instructions
  • It can let the JVM know that the variable is "special" when reading the variable, and force the value of the variable to be read from the memory every time.
  • Thus improving thread safety and prohibiting instruction reordering

Guess you like

Origin blog.csdn.net/HBINen/article/details/126593368