Although multithreading can greatly improve the CPU execution efficiency, it is not without harm. There are also thread insecurity, which is also the most important and complex problem involved in multithreading concurrency.
So what is thread unsafe?
To sum up, the reason for thread insecurity is that logical errors occur when multiple lines of code are executed concurrently, which is called thread insecurity. So what is a logical error? We explain it with specific code
class counter {//Counter public int count = 0 ; public void incr() { count++; } } public class ThreadDemo12 {//The authentication thread is unsafe public static void main(String[] args) throws InterruptedException { counter co = new counter(); Thread t1 = new Thread() { @Override public void run() { for(int i = 0; i<5000;i++){ co.incr(); } } }; Thread t2 = new Thread() { @Override public void run() { for(int i = 0; i<5000;i++){ co.incr(); } } }; t1.start(); t2.start(); t1.join(); t2.join(); System.out.println((co.count)); } //t1 thread will modify the count of the counter, and t2 thread will also modify it. And they are executed in parallel, which will lead to the final count //It does not reach 5000 of t1 and 5000 of t2 This is called thread insecurity
Through the above code, we will find that when multiple threads modify or access a variable at the same time, there will be wrong results. The main reasons are as follows
- The concurrent execution of multiple threads is preemptive scheduling, which means that our user layer cannot control or know the execution order of threads. It is entirely up to the scheduler to decide. It is possible that a thread is being pulled down when it is being executed by the CPU.
- The auto increment operation is not atomic, that is, the auto increment is not executed at one time or not at all. Instead, it is divided into three steps. The first step is to read the data to be calculated from the memory to the CPU, then calculate by the CPU, and finally return the results to the memory.
The combination of the above two reasons leads to the wrong result. Suppose there is a CPU at this time, t1 thread and t2 thread are started at the same time. When the CPU completes the reading operation in t1, it switches to t2 thread and reads to the CPU. Then switch to t1 to continue. In this way, t1 and t2 each get a + 1 result. The final result is 1 instead of 2. Of course, it is also possible that t1 just performs the return operation and t2 just performs the read operation. The result of such a series is just right.
Through the above example, we will find that there are three main reasons for thread insecurity
- Thread scheduling is preemptive, which is the main reason for thread insecurity.
- Some operations are not atomic, that is, they cannot be executed on the CPU at one time, or they are not executed on the CPU for the time being
- Multiple threads try to modify the same variable, but if there is one-to-one modification, many-to-one reading, one-to-one reading and many-to-many modification between threads and variables, these are safe.
In addition, there are two reasons why threads are unsafe
- Memory visibility
- Reordering instructions: when compiling code, the Java compiler will optimize the instructions and adjust the order of instructions to improve the operation efficiency of the program without changing the original logic.
In order to solve the problem of thread insecurity, the method of obtaining and releasing locks is introduced. First, let's understand what locks are
Lock features: mutually exclusive. At the same time, only one thread can obtain the lock of the same object at any time. If other threads try to acquire, they will block and wait until the thread releases the lock just now, and then they will participate in the competition again.
Basic operation of lock:
- Lock
- Unlock
In Java, the use of locks requires the keyword synchronized
class counter {//Counter public int count = 0 ; synchronized public void incr() { count++; } } public class ThreadDemo12 {//The authentication thread is unsafe public static void main(String[] args) throws InterruptedException { counter co = new counter(); Thread t1 = new Thread() { @Override public void run() { for(int i = 0; i<5000;i++){ co.incr(); } } }; Thread t2 = new Thread() { @Override public void run() { for(int i = 0; i<5000;i++){ co.incr(); } } }; t1.start(); t2.start(); t1.join(); t2.join(); System.out.println((co.count)); }
We know that after an object is new, memory will be allocated on the heap to save some information of the object, including some hidden information. Among them, there is a locked state. We can imagine that this state is of boolean type. When thread 1 and thread 2 preemptive execution, who will be dispatched first will call the method or class modified by synchronized first, so that the thread will lock the object's state to true. When other threads want to try to modify it, they will enter the blocking state. They must wait for the execution of the incr method called in the thread 1 to complete the competition again. It can be understood as changing an operation that is not atomic into an atomic operation.
It should be noted that. If there are some accidents after thread 1 obtains the lock, resulting in that it cannot be unlocked for a long time, the threads competing for the lock will not continue to execute and can only continue to block. In extreme cases, deadlock occurs. The program is cool. And once the lock is used, the program is basically out of touch with high performance, because the process of waiting for unlocking is time-consuming.
Several common uses of synchronized
- Adding in front of a method means locking the class where the method is located, that is, this
- It is added before the static method to represent the class object of the lock class.
- Before adding to a code block, displays the lock assigned to an object. It should be noted that mutex occurs only when the competing locks between threads are the same lock, that is, the locking state of the same object or class object.