当前位置:网站首页>[thread safety] what risks may multithreading bring?

[thread safety] what risks may multithreading bring?

2022-07-28 14:43:00 Caixinzhi

1. The goal is

         The ultimate goal of this article is to master the situation of multithreading , There will be security problems , And why such problems occur , Finally, the corresponding solution is introduced .

2. What is thread safety

         Thread safety is the result of executing code in the case of multithreading, if and as expected ( The result of execution in the case of single thread ) The same as , So it's thread safety . otherwise , It's just that the thread is not safe , At this time, thread safety problems are likely to occur .

3. Thread safety problems and the causes of thread insecurity

         Before exploring thread safety , Here is an example of thread insecurity and the running results to lead to the following :

// Create two threads , Let these two threads execute a variable concurrently , Self increase respectively 5w Time ,  Finally, it is estimated that the total self increase 10w Time 
class Counter{
    
    // Variables that hold counts 
    public int count;

    public void increase(){
    
        count++;
    }
}

public class Main {
    
    public static void main(String[] args) {
    
        Counter counter=new Counter();

        Thread thread1=new Thread(() -> {
    
            for(int i=0;i<50000;i++){
    
                counter.increase();
            }
        });

        Thread thread2=new Thread(() -> {
    
            for(int i=0;i<50000;i++){
    
                counter.increase();
            }
        });

        thread1.start();
        thread2.start();

        try {
    
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
    
            e.printStackTrace();
        }

        System.out.println("count="+counter.count);
    }
}

         Running results :
 Insert picture description here

         Here we will find : After we run this code , The results are less than 10 Ten thousand , And the results of each run are different ( But the probability is 5 and 10 Between ten thousand , The specific reasons will be explained later ), It's completely different from what we expected , What we want is to use multithreading to improve running efficiency and achieve the target effect , And the results of the operation are similar to 10 Obviously, there is a big difference . Why is that ?

3.1 Thread safety problem 1 : Random scheduling of operating system

         Random scheduling of operating system ( Or preemptive execution ) Is the most fundamental cause of thread insecurity . Because of the thread thread1 And thread thread2 It's concurrent , So when these two threads are executed concurrently in the operating system , It may happen that two threads read the same data in memory at the same time , But finally, after the two threads are executed , What is stored in memory will only be the value stored in memory ( This is just one of the possible situations , It may also occur before a thread stores the calculated data in memory , Another thread is already reading data in memory …), In short, the random scheduling of this operating system makes the order of executing threads random , Never guess what the next instruction in the operating system will execute , Very complicated … This is one of the reasons why threads are unsafe , It is also the most fundamental reason .

3.2 Thread safety problem 2 : Multiple threads modify the same variable

         Just like the above , Two threads (CPU) Come to the same piece ( Memory ) Variable to modify ( In the example, add ) When , Thread safety problems are likely to occur . Be careful : There are three key points — 1. Multiple threads -> 2. For the same variable -> 3. And it must be modified ( Pure read operation will not cause thread safety problems ). In the end, this is also the random scheduling of the operating system ( uncertainty ) Caused by , Because if only a thread modifies variables ( This is equivalent to ordinary code written before , There is no need to consider thread safety ), Or multiple threads read the same variable , Or multiple threads to modify multiple variables ( This is equivalent to each thread performing its own duties , Disguised single thread ), Will not cause thread safety problems , When multiple threads modify the same variable , The order in which the operating system reads and writes memory is unknown , This is one of the reasons why threads are unsafe .

3.3 Thread safety problem 3 : The modification operation is not atomic

         In the above code , Counter Class increase() Every time the method is executed ++ operation , The bottom layer of the operating system will carry out three-step operations ( Three instructions ): 1. Load data in memory CPU(LOAD); 2. stay CPU Perform addition operation in (ADD); 3. Store the calculated results in memory (SAVE). We regard these three operations as a modification operation . Atomicity comes before MySQL Things in ( The core characteristic of things ) Just introduced , In short , Atomicity is to regard some operations as an inseparable whole .
         that , Why is it that if the modification operation is not atomic, it may lead to thread insecurity ?
         In fact, this is also caused by the random scheduling of the operating system . If the above three steps are separated , If it's just a single thread , It won't make any difference , But there are two threads in the sample code , In terms of time sequence , These three steps ( Three instructions ) It is very likely to be executed in a staggered way , This will cause the modified result to be wrong . This is one of the reasons why threads are unsafe .

3.4 Thread safety problem 4 : Memory visibility

         The memory visibility problem is actually when the operating system optimizes the code , Thread safety problems caused by . If there are some operations in the thread that have been doing a certain work repeatedly , At this time, the operating system may optimize it , Make memory invisible , Instead, read the contents of the register directly , This may omit some repeated computer instructions , Keep valid instructions . The most classic is in the case of single thread , We're executing ++ When , Need experience LOAD , ADD , SAVE Three instructions can be completed , But if you are executing a loop ++ During operation , The operating system will optimize the three instructions that have been looping into LOAD , ADD , ADD , ADD … , ADD , SAVE . in other words , The original three instruction loop is optimized to run only once LOAD and SAVE , And in the ADD A cycle is carried out on , This can greatly reduce the time of repeatedly reading and writing memory , It improves the efficiency of calculation . Of course , In such a single threaded case , There will be no safety problems , The results are also correct , But in the case of multithreading , There is likely to be a problem , Here is a classic example ( The following code ):

public class Main {
    
    public static int flag=0;

    public static void main(String[] args) {
    
        Thread thread1=new Thread(() -> {
    
            while(flag==0){
    
                try {
    
                    ;
                } catch (InterruptedException e) {
    
                    e.printStackTrace();
                }
            }
            System.out.println(" The loop ends ");
        });

        Thread thread2=new Thread(() -> {
    
            Scanner scanner=new Scanner(System.in);
            System.out.println(" Please enter an integer ");
            flag=scanner.nextInt();
        });

        thread1.start();
        thread2.start();
    }
}

         Because in the thread thread1 Repeat in flag The value of , And the judgment is true ( Into the circulatory body ), Now , When optimizing the operating system , This step may be omitted , This leads to if in the thread thread2 Yes flag Changing the value will not make the thread thread1 Out of the loop . This is one of the reasons why threads are unsafe .

3.5 Thread safety problem five : The instructions are reordered

         Instruction reordering is also a thread safety problem in the process of operating system optimization . Instruction reordering is actually an optimal solution of the logical order of instruction execution that the operating system helps us find , So as to improve the efficiency of code execution . Like , There are several instructions that can complete a thing , But when you change the order of these instructions , You may get a better solution , Now , The operating system is likely to optimize these instructions directly , To improve code execution efficiency . Of course , If this is in the case of a single thread, it must be all right , Because no matter how the order of instructions is adjusted , The final result of the implementation is still like that , It's just an optimization . But in the case of multithreading , Adjustment of instruction sequence , Is the difference that will cause the final execution result , This is one of the reasons why threads are unsafe .

4. The solution to the above thread safety problem

4.1 in the light of Question 1

         For problem 1, the random scheduling of operating system , We have no way to solve it , This random scheduling of the operating system in multithreading is very annoying , All we can do is avoid it when necessary , It is impossible and impossible to modify the random scheduling feature of the operating system .

4.2 in the light of Question two

         For problem 2, multiple threads modify the same variable , In fact, we can adjust the structure of the code directly , Don't let multiple threads modify the same variable .
         But someone here asked : If I have to modify the same variable through multithreading , Is there any solution ? The answer you want is below , Please read on .

4.3 in the light of Question 3

         For problem 3, the modification operation is not atomic , Take the first code above , Here's our solution : take LOAD ADD SAVE These three instructions perform lock operation ( That is to pack these three instructions together , This is an inseparable whole , There will be no thread safety problems ).
         stay Java There are many kinds of locking operations in , Here is a common locking method : Use synchronized keyword .

4.3.1 Detailed explanation synchronized keyword

         there synchronized Keywords are " Sync " It means , Of course , there " Sync " No IO The scene or the upper and lower levels call the scene " Sync " and " asynchronous ". here " Sync " It means " Mutually exclusive ", in other words , If you add synchronized keyword , Then it is equivalent to locking this method , When calling this method in a thread , Because of the lock , So this is when other threads want to call this method again , You need to block and wait , Until the method call ends and the lock is unlocked , Can be called by other threads , In this way " Mutually exclusive " The effect of . Of course , " Sync " There are other meanings , Here is a supplementary point : stay IO Under the scenario, or the upper and lower levels call under the scenario , " Sync " It means that the caller is responsible for the operation of obtaining the call result ; " asynchronous " It means that the caller is not responsible for obtaining the call result , Instead, the callee actively pushes the calculated result to the caller .
         Use synchronized There are two cases of keyword locking :
        1. Lock the object . When locking objects , There are two ways of writing :
        (1) Direct modification of common methods , The sample code is as follows :

public class SynchronizedDemo {
    
	synchronized public void methond() {
    
		...
	}
}

        (2) Use specified this Decorated code block , The sample code is as follows :

public class SynchronizedDemo {
    
	public void method() {
    
		synchronized (this) {
    
			...
		}
	}
}

         Be careful : The above two are just written differently , The end result is the same , So these two ways of writing are equivalent .

        2. Lock class objects . When locking class objects , There are also two ways of writing :
        (1) Direct modification of static methods , The sample code is as follows :

public class SynchronizedDemo {
    
	synchronized public static void method() {
    
		...
	}
}

        (2) Use the specified class .class Decorated code block , Examples are as follows :

public class SynchronizedDemo {
    
public void method() {
    
		synchronized (SynchronizedDemo.class) {
    
			...
		}
	}
}

         Be careful : (1) The above two are just written differently , The end result is the same , So these two ways of writing are equivalent . (2) The second method specifies the class .class It is not necessary to use this class ( The class corresponding to this method ), It can also be other classes .


         In the use of synchronized When locking keywords , We just need to recognize : Two threads must be the same lock , Will happen blocking and waiting , Otherwise, there will be no blocking waiting , Because they don't point to the same lock ( It can be understood that these two threads are not atomic ). that , How can we judge whether two threads point to the same lock ?
         Situation 1 : Lock the object . If the locking method in the same object is used in two threads , Then the thread executing later will block and wait . But if two threads use different object locking methods , Then there will be no blocking and waiting . for instance :

class A{
    
    synchronized public void m1(String a){
    
        System.out.println(a+" Start m1");
        try {
    
            Thread.sleep(3000);
        } catch (InterruptedException e) {
    
            e.printStackTrace();
        }
        System.out.println(a+" end m1");
    }

    synchronized public void m2(String a){
    
        System.out.println(a+" Start m2");
        try {
    
            Thread.sleep(3000);
        } catch (InterruptedException e) {
    
            e.printStackTrace();
        }
        System.out.println(a+" end m2");

    }
}

public class Main {
    
    public static void main(String[] args) {
    
        A a1=new A();
        A a2=new A();

        Thread thread1=new Thread(() -> {
    
            a1.m1(" Threads 1");
        });

        Thread thread2=new Thread(() -> {
    
            a2.m1(" Threads 2");
        });

        thread1.start();
        thread2.start();
    }
}

Running results :
 Insert picture description here

         Although there are synchronized modification , But because of the two threads used separately a1 and a2 It's two different objects , So they will not block and wait .

class A{
    
    synchronized public void m1(String a){
    
        System.out.println(a+" Start m1");
        try {
    
            Thread.sleep(3000);
        } catch (InterruptedException e) {
    
            e.printStackTrace();
        }
        System.out.println(a+" end m1");
    }

    synchronized public void m2(String a){
    
        System.out.println(a+" Start m2");
        try {
    
            Thread.sleep(3000);
        } catch (InterruptedException e) {
    
            e.printStackTrace();
        }
        System.out.println(a+" end m2");

    }
}

public class Main {
    
    public static void main(String[] args) {
    
        A a1=new A();
        A a2=new A();

        Thread thread1=new Thread(() -> {
    
            a1.m1(" Threads 1");
        });

        Thread thread2=new Thread(() -> {
    
            a1.m2(" Threads 2");
        });

        thread1.start();
        thread2.start();
    }
}

Running results :
 Insert picture description here

         Because both threads use the method of locking the same object , So there will be blocking and waiting , Operations like this are thread safe .

         Situation two : Lock class objects . If the lock of the method called in two threads points to the same class , So even though they don't use the same object , There will also be blocking waiting ( That's thread safety ). So you could say , The key point of locking class objects is to see whether the lock of calling methods between different threads points to the same class .

class A{
    
    synchronized public static void m1(String a){
    
        System.out.println(a+" Start m1");
        try {
    
            Thread.sleep(3000);
        } catch (InterruptedException e) {
    
            e.printStackTrace();
        }
        System.out.println(a+" end m1");
    }

    synchronized public static void m2(String a){
    
        System.out.println(a+" Start m2");
        try {
    
            Thread.sleep(3000);
        } catch (InterruptedException e) {
    
            e.printStackTrace();
        }
        System.out.println(a+" end m2");

    }
}

public class TestDemo4 {
    
    public static void main(String[] args) {
    
        Thread thread1=new Thread(() -> {
    
            A.m1(" Threads 1");
        });

        Thread thread2=new Thread(() -> {
    
            A.m2(" Threads 2");
        });

        thread1.start();
        thread2.start();
    }
}

Running results :
 Insert picture description here

         Of course , Locking class objects can also be for different classes , Blocking and waiting can also occur between two threads . Another example :

class B{
    
    public void m(String a){
    
        synchronized (B.class){
    
            System.out.println(a+" Start ");
            try {
    
                Thread.sleep(3000);
            } catch (InterruptedException e) {
    
                e.printStackTrace();
            }
            System.out.println(a+" end ");
        }
    }
}

class C{
    
    public void m(String a){
    
        synchronized (B.class){
    
            System.out.println(a+" Start ");
            try {
    
                Thread.sleep(3000);
            } catch (InterruptedException e) {
    
                e.printStackTrace();
            }
            System.out.println(a+" end ");
        }
    }
}

public class TestDemo5 {
    
    public static void main(String[] args) {
    
        B b=new B();
        C c=new C();

        Thread thread1=new Thread(() -> {
    
            b.m(" Threads 1");
        });

        Thread thread2=new Thread(() -> {
    
            c.m(" Threads 2");
        });

        thread1.start();
        thread2.start();
    }
}

Running results :
 Insert picture description here

4.3.2 Use synchronized Keyword in solving the problem of problem three

         By facing up to synchronized After knowing the keywords , Solve problem 3. If the modification operation is not atomic, it will become very simple , Directly in execution ++ The method of operation plus synchronized Key words can be used . The specific code is as follows :

public class TestDemo {
    
    static class Counter{
    
        public int count;

        synchronized public void crease(){
    
            count++;
        }
    }

    public static void main(String[] args) {
    
        Counter counter=new Counter();

        Thread thread1=new Thread(() -> {
    
            for(int i=0;i<10000;i++){
    
                counter.crease();
            }
        });

        Thread thread2=new Thread(() -> {
    
            for(int i=0;i<10000;i++){
    
                counter.crease();
            }
        });

        thread1.start();
        thread2.start();

        try {
    
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
    
            e.printStackTrace();
        }

        System.out.println("count="+counter.count);
    }
}

4.4 in the light of Question 4 + Question five

         For problem 4 memory visibility and problem 5 instruction reordering , In fact, they are all problems caused by multithreading after the operating system optimizes the code , Just like the code in question 4 above . So face this problem , How can we solve this problem ? When multithreading , We can use volatile Keyword to prevent ( prohibit ) The operating system optimizes the code ( That is to make the memory from invisible to visible and prevent the reordering of instructions ), This keyword is actually adding a special binary instruction to the added variable — “ Memory protection ”. So the modified code can be :

public class Main {
    
    volatile public static int flag=0;

    public static void main(String[] args) {
    
        Thread thread1=new Thread(() -> {
    
            while(flag==0){
    
                try {
    
                    ;
                } catch (InterruptedException e) {
    
                    e.printStackTrace();
                }
            }
            System.out.println(" The loop ends ");
        });

        Thread thread2=new Thread(() -> {
    
            Scanner scanner=new Scanner(System.in);
            System.out.println(" Please enter an integer ");
            flag=scanner.nextInt();
        });

        thread1.start();
        thread2.start();
    }
}

         add volatile after , There will be no optimization operation , The result of the operation is correct . It's a way , There is another way to avoid volatile Keywords will not be optimized , That is in this code thread thread1 Add sleep Method to block the code , At this time, the code cycle speed will slow down a little , The operation of reading and writing memory will not become so frequent , It will not trigger code optimization ( But here's the suggestion : Try to add volatile keyword , In order to avoid unnecessary impact of some optimizations on code logic ). The specific code is as follows :

public class Main {
    
    public static int flag=0;

    public static void main(String[] args) {
    
        Thread thread1=new Thread(() -> {
    
            while(flag==0){
    
                try {
    
                    Thread.sleep(100);
                } catch (InterruptedException e) {
    
                    e.printStackTrace();
                }
            }
            System.out.println(" The loop ends ");
        });

        Thread thread2=new Thread(() -> {
    
            Scanner scanner=new Scanner(System.in);
            System.out.println(" Please enter an integer ");
            flag=scanner.nextInt();
        });

        thread1.start();
        thread2.start();
    }
}

        volatile There are two main functions of keywords :
        (1) Ensure memory visibility : Implement based on barrier instructions , That is, when a thread modifies a shared variable , Another thread can read the modified value .
        (2) To ensure order : Disable instruction reordering . Compile time JVM The compiler follows the constraints of the memory barrier , When running, the order of instructions is organized by barrier instructions .
         One more thing to note : volatile Atomicity cannot be guaranteed .

         Speaking of optimization , Here is a brief talk about the above optimization process , stay Java It is also called "JMM(Java Memory Model)".

 Insert picture description here

         The reason for the problem after optimization is : After thread optimization , Mainly in the operation of working memory , Failed to read the main memory in time , Thus leading to the phenomenon of misjudgment . among , Working memory here refers to CPU The register of ( May include CPU cache ); The main memory here is what the computer calls the real memory . therefore , We can also simplify the above paragraph into : After thread optimization , Mainly in operation CPU, Failed to read memory in time , Thus leading to the phenomenon of misjudgment .

5. summary

         front MySQL The things mentioned in this article are very similar to multithreading , In fact, from a certain phenomenon , Things can be a simplified version of multithreading , They are all some problems that will occur during the execution of concurrency , And after solving these problems , Will make the code accurate ( Or isolation ) Improve , But it will sacrifice some operating efficiency .
         But then again , To make a long story short , Multithreading does bring some risks , For this, we should be more bold and careful when writing code , ctively , Reduce the problems caused by multithreading bug The situation of .

原网站

版权声明
本文为[Caixinzhi]所创,转载请带上原文链接,感谢
https://yzsam.com/2022/209/202207281328389678.html