What is thread safety
Brian Goetz, the author of Java Concurrency In Practice, understands thread safety in this way. When multiple threads access an object, if the scheduling and alternate execution of these threads in the runtime environment are not considered, and there is no need for additional synchronization, the behavior of calling this object can obtain the correct results, Then this object is thread safe.
Generally speaking, in the case of multithreading concurrency, the execution result of each thread is always the expected result. Then this thread is thread safe.
There are three situations of thread safety
After understanding the above concepts, we will find that we often encounter thread insecurity during normal development. The following three types are listed
- Operation result error
- Thread safety issues due to publishing and initialization
- Activity problem
Operation result error
First, let's look at the following code
public class WrongResult { private static volatile int count = 0; public static void main(String[] args) throws InterruptedException { Runnable runnable = new Runnable() { @Override public void run() { for (int i = 0; i < 1000; i++) { count++; } } }; Thread t0 = new Thread(runnable); Thread t1 = new Thread(runnable); t0.start(); t1.start(); t0.join(); t1.join(); System.out.println(count); } }
The logic of the above code is that two threads add one to count, and each thread will cycle to add 1000 times. Then the expected result should be 2000. In fact, the result of implementation alone is not 2000, and it will always be less than 2000. What is the reason for this.
Here we need to understand the allocation of computer CUP scheduling. In the allocation rules of CUP, each thread is allocated in time. If the allocated time of the current thread is completed, it will be switched to another thread for execution. The bad thing is that it can't guarantee the atomicity of i +. Let's take a look at the following figure first.
From the above figure, we can see that the steps i + + performs in the cup are actually divided into three steps
- The first step is to read the value of i
- The second step is to add one
- The third step is to save the value of the result
In this way, we can think carefully. If the time allocated by CUP to thread 1 is only enough for it to execute the second step, and then it is switched to thread 2, then thread 2 will not obtain the latest value, because thread 1 has not executed the third step to save the results, and the CPU will cut it off. At this time, the value calculated by thread 2 will be the same as that obtained by thread 1. Naturally, we will get much less results than expected. This situation is the most typical thread safety problem.
Publishing and initialization cause thread safety problems
This is easier to understand. For example, when the project is started, you need to obtain a piece of initial data, but this data needs to be initialized asynchronously through threads. However, it takes time for the thread to initialize the data. If the program obtains the data after the thread has not been initialized, it will lead to thread safety problems. You can see the following code to understand.
public class WrongInit { private List<String> info; public WrongInit() { info = new ArrayList<>(); Thread thread = new Thread(()->{ info.add("Data initialization started"); try { info.add("Data initialization in progress...."); //Time required for analog data initialization TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } info.add("Data initialization completed!"); }); thread.start(); } public static void main(String[] args) throws InterruptedException { WrongInit init = new WrongInit(); init.info.forEach(System.out::println); } }
In the above code, in order to simulate the situation that the data is not fully initialized, the thread sleeps for 1 second. In fact, the data should be printed out [data initialization completed!] However, the results are only printed to [data initialization...], In fact, there will be another case here. If the time to create a thread is greater than the time to call the program, a null pointer exception may be reported directly. This is also a thread unsafe situation.
Activity problem
Thread activity can be divided into three types: deadlock, livelock and starvation.
These three have a common feature, that is, they will make the thread get stuck, and there will be no running results. In fact, this situation is the most complex and most serious in thread safety. If the thread gets stuck too much, it will not only occupy the resources of the service, but also lead to the false death or downtime of the service.
deadlock
Deadlocks are common, that is, two threads wait for each other's resources, but they don't give in to each other at the same time. They all want to execute first. You can see the following code
public class ThreadDeadMain { private static Object o1 = new Object(); private static Object o2 = new Object(); public static void main(String[] args) { Thread thread01 = new Thread(()->{ synchronized (o1){ System.out.println(Thread.currentThread().getName()+"Get o1 The door is locked"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (o2){ System.out.println(Thread.currentThread().getName()+"Get o2 My lock is locked"); } } }); Thread thread02 = new Thread(()->{ synchronized (o2){ System.out.println(Thread.currentThread().getName()+"Get o2 My lock is locked"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (o1){ System.out.println(Thread.currentThread().getName()+"Get o1 My lock is locked"); } } }); thread01.start(); thread02.start(); } }
In the above code, two threads are started. There are two locks in each thread. When thread 1 starts, it will first obtain the lock of o1, then obtain the lock of o2, and then release the lock of o2 and o1. The logic of thread 2 relative to thread 1 is the opposite. It shows that it obtains the lock of o2, then obtains the lock of o1, and finally releases the lock of o2 and then releases the lock of o1. In order to make two threads deadlock, I let the thread sleep for one second when both threads acquire the first lock. In this way, when two threads acquire the second lock at the same time, it will be found that the o2 of the second lock of thread 1 is not released in the first lock of thread 2, but the second lock o1 of thread 2 is occupied by thread 1, In this way, the two will not yield to each other and occupy the lock at the same time. Cause the program to be stuck all the time.
Livelock
A livelock has the opposite meaning to a deadlock. A deadlock is a resource stuck. A livelock is different. It does not occupy the lock's resources, but it will run all the time. However, it will run in a loop all the time, but it has not encountered the correct result, resulting in the thread running all the time. But it won't get stuck like a deadlock.
You can see the following code
public class ThreadLiveMain implements Runnable { private int num; public ThreadLiveMain(int num) { this.num = num; } @Override public void run() { System.out.println("Winning number:"+num); int i = 0; do { //Random numbers 1-10 Random random = new Random(); i = random.nextInt(10)+1; System.out.println("Random number:"+i); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } }while (num!=i); System.out.println("Yes!"); } public static void main(String[] args) { ThreadLiveMain main = new ThreadLiveMain(11); Thread thread = new Thread(main); thread.start(); } }
This is a small program similar to the lottery. The thread will randomly give a number of 1-10 to judge whether it wins the prize. If we give the winning number within 1-10, the program will get normal results at this time, because it is within the range of random numbers. But if we give an 11, it's beyond the range of random numbers. The thread will always go through the loop judgment, but it cannot meet the correct random number, so the thread will not stop. This situation is called livelock.
hunger
Hunger is more interesting. It's really because of hunger that threads can't get results. Java threads have the concept of priority, with a priority division of 1-10. If the level of a thread is set to the lowest 1, then the thread may never get the resources of the thread. The thread can't eat and naturally has no strength to work, If you don't work hard, you won't get results. Another situation is that a thread holds the lock of a file. If other threads want to modify the file, they need to obtain the lock of the file first. Then the thread modifying the file will be hungry and can't continue to run.