This article mainly follows two previous articles of multi-threading to summarize thread safety issues in Java multi-threading.
1. A typical Java thread safety example
public class ThreadTest { public static void main(String[] args) { Account account = new Account("123456", 1000); DrawMoneyRunnable drawMoneyRunnable = new DrawMoneyRunnable(account, 700); Thread myThread1 = new Thread(drawMoneyRunnable); Thread myThread2 = new Thread(drawMoneyRunnable); myThread1.start(); myThread2.start(); }}class DrawMoneyRunnable implements Runnable { private Account account; private double drawAmount; public DrawMoneyRunnable(Account account, double drawAmount) { super(); this.account = account; this.drawAmount = drawAmount; } public void run() { if (account.getBalance() >= drawAmount) { //1 System.out.println("The withdrawal was successful, the withdrawal of the money is: " + drawAmount); double balance = account.getBalance() - drawAmount; account.setBalance(balance); System.out.println("Balance is: " + balance); } }}class Account { private String accountNo; private double balance; public Account() { } public Account(String accountNo, double balance) { this.accountNo = accountNo; this.balance = balance; } public String getAccountNo() { return accountNo; } public void setAccountNo(String accountNo) { this.accountNo = accountNo; } public double getBalance() { return balance; } public void setBalance(double balance) { this.balance = balance; }}The above example is easy to understand. There is a bank card with a balance of 1,000. The program simulates the scene where you and your wife are withdrawing money at the ATM at the same time. Run this program multiple times and may have output results in multiple different combinations. One of the possible outputs is:
1 The withdrawal of money is successful, the withdrawal of money is: 700.0
2 Balance is: 300.0
3 The withdrawal of money is successful, the withdrawal of money is: 700.0
4 The balance is: -400.0
In other words, for a bank card with a balance of only 1,000, you can withdraw a total of 1,400, which is obviously a problem.
After analysis, the problem lies in the uncertainty of execution in a Java multi-threaded environment. The CPU may randomly switch between multiple threads in the ready state, so it is very likely that the following situation occurs: When thread1 executes the code at //1, the judgment condition is true. At this time, the CPU switches to thread2, executes the code at //1, and finds that it is still true. Then, thread2 is executed, then switch to thread1, and then the execution is completed. At this time, the above results will appear.
Therefore, when it comes to thread safety issues, it actually means that accessing shared resources in a multi-threaded environment may cause inconsistency in this shared resource. Therefore, to avoid thread safety issues, concurrent access to this shared resource in a multi-threaded environment should be avoided.
2. Synchronization method
The synchronized keyword modification is added to the method definition for accessing shared resources, making this method called the synchronization method. It can be simply understood that this method is locked, and its locked object is the object itself where the current method is located. In a multi-threaded environment, when executing this method, you must first obtain this synchronization lock (and at most only one thread can obtain it). Only when the thread executes this synchronization method will the lock object be released, and other threads can obtain this synchronization lock, and so on...
In the above example, the shared resource is an account object, and when using the synchronization method, it can solve thread safety issues. Just add the synshronized keyword before the run() method.
public synchronized void run() { // .... }3. Synchronize code blocks
As analyzed above, solving thread safety problems only requires limiting the uncertainty of access to shared resources. When using the synchronization method, the entire method body becomes a synchronous execution state, which may cause the synchronization range to occur. Therefore, another synchronization method - the synchronization code block - can be solved directly for the code that needs synchronization.
The format of the synchronous code block is:
synchronized (obj) { //... }Among them, obj is the lock object, so it is crucial to choose which object to be locked. Generally speaking, this shared resource object is selected as the lock object.
As in the above example, it is best to use the account object as the lock object. (Of course, it is also possible to choose this, because the creation thread uses the runnable method. If it is a thread created directly inheriting the Thread method, using this object as a synchronization lock will actually not play any role because it is a different object. Therefore, you need to be extra careful when choosing a synchronization lock...)
4.Lock object synchronization lock
As we can see above, precisely because we need to be so careful about the selection of synchronous lock objects, is there any simple solution? It can facilitate the decoupling of synchronous lock objects from shared resources, while also solving thread safety problems well.
Using Lock object synchronization locks can easily solve this problem. The only thing to note is that the Lock object needs to have a one-to-one relationship with the resource object. The general format of the Lock object synchronization lock is:
class X { // Display the object that defines the Lock synchronization lock, which has a one-to-one relationship with the shared resource private final Lock lock = new ReentrantLock(); public void m(){ // Lock lock.lock(); //... Code that requires thread-safe synchronization // Release the Lock lock lock.unlock(); } }5.wait()/notify()/notifyAll() thread communication
These three methods are mentioned in the blog post "Java Summary Series: java.lang.Object". Although these three methods are mainly used in multithreading, they are actually local methods in the Object class. Therefore, theoretically, any Object object can be used as the main tone of these three methods. In actual multi-threading programming, only by synchronizing the lock object to tune these three methods can thread communication between multiple threads be completed.
wait(): causes the current thread to wait and make it enter a wait blocking state. Until another thread calls the notify() or notifyAll() method of the synchronous lock object to wake up the thread.
notify(): Wake up a single thread waiting on this synchronous lock object. If multiple threads are waiting on this synchronous lock object, one of the threads will be selected for wake-up operation. Only when the current thread abandons the lock on the synchronous lock object can the awakened thread be executed.
notifyAll(): Wake up all threads waiting on this synchronous lock object. Only when the current thread abandons the lock on the synchronous lock object can the awakened thread be executed.
package com.qqyumidi;public class ThreadTest { public static void main(String[] args) { Account account = new Account("123456", 0); Thread drawMoneyThread = new DrawMoneyThread("Get Money Thread", account, 700); Thread depositMoneyThread = new DepositMoneyThread("Save Money Thread", account, 700); drawMoneyThread.start(); depositMoneyThread.start(); }}class DrawMoneyThread extends Thread { private Account account; private double amount; public DrawMoneyThread(String threadName, Account account, double amount) { super(threadName); this.account = account; this.amount = amount; } public void run() { for (int i = 0; i < 100; i++) { account.draw(amount, i); } }}class DepositeMoneyThread extends Thread { private Account account; private double amount; public DepositMoneyThread(String threadName, Account account, double amount) { super(threadName); this.account = account; this.amount = amount; } public void run() { for (int i = 0; i < 100; i++) { account.deposit(amount, i); } }}class Account { private String accountNo; private double balance; // Identify whether there is already a deposit in the account private boolean flag = false; public Account() { } public Account(String accountNo, double balance) { this.accountNo = accountNo; this.balance = balance; } public String getAccountNo() { return accountNo; } public void setAccountNo(String accountNo) { this.accountNo = accountNo; } public double getBalance() { return balance; } public void setBalance(double balance) { this.balance = balance; } /** * Save money* * @param depositAmount */ public synchronized void deposit(double depositAmount, int i) { if (flag) { // Someone in the account has already saved money, and the current thread needs to wait to block try { System.out.println(Thread.currentThread().getName() + " Start to execute wait operation" + " -- i=" + i); wait(); // 1 System.out.println(Thread.currentThread().getName() + " Performed wait operation" + " -- i=" + i); } catch (InterruptedException e) { e.printStackTrace(); } } else { // Start saving System.out.println(Thread.currentThread().getName() + " Deposit:" + depositAmount + " -- i=" + i); setBalance(balance + depositAmount); flag = true; // Wake up other threads notifyAll(); // 2 try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "-- Save money-- Execution is completed" + " -- i=" + i); } } /** * Withdraw money* * @param drawAmount */ public synchronized void draw(double drawAmount, int i) { if (!flag) { // No one in the account has saved money yet, and the current thread needs to wait to block try { System.out.println(Thread.currentThread().getName() + " Start to execute wait operation" + " executed wait operation" + " -- i=" + i); wait(); System.out.println(Thread.currentThread().getName() + "Execute wait operation" + "Execute wait operation" + " -- i=" + i); } catch (InterruptedException e) { e.printStackTrace(); } } else { // Start withdrawing money System.out.println(Thread.currentThread().getName() + " Withdraw money: " + drawAmount + " -- i=" + i); setBalance(getBalance() - drawAmount); flag = false; // Wake up other threads notifyAll(); System.out.println(Thread.currentThread().getName() + "-- Withdraw money--Execution is completed" + " -- i=" + i); // 3 } }} The above example demonstrates the usage of wait()/notify()/notifyAll(). Some output results are:
The money withdrawal thread starts to execute the wait operation and execute the wait operation-- i=0
Saving thread deposit: 700.0 -- i=0
Save money thread-Save money-Execution--i=0
The money saving thread needs to perform wait operation-- i=1
The money withdrawal thread executes wait operation and wait operation-- i=0
Withdraw money thread withdraw money: 700.0 -- i=1
Money withdrawal thread--withdrawal--execution--i=1
The thread to withdraw money must start to execute the wait operation and execute the wait operation-- i=2
The money saving thread executes wait operation-- i=1
Saving thread deposit: 700.0 -- i=2
Save money thread-Save money-Execution--i=2
The withdrawal thread executes the wait operation and executes the wait operation-- i=2
Withdraw money thread withdrew money: 700.0 -- i=3
Money withdrawal thread--withdrawal--execution--i=3
The thread to withdraw money must execute the wait operation and execute the wait operation-- i=4
Saving thread deposit: 700.0 -- i=3
Save money thread-Save money-Execution--i=3
The money saving thread needs to perform wait operation-- i=4
The money withdrawal thread executes wait operation and wait operation-- i=4
Withdraw money thread withdrew money: 700.0 -- i=5
Money withdrawal thread--withdrawal--execution--i=5
The thread to withdraw money must start to perform wait operation and execute wait operation-- i=6
The money saving thread executes wait operation-- i=4
Saving thread deposit: 700.0 -- i=5
Save money thread-Save money-Execution--i=5
The money saving thread needs to perform wait operation-- i=6
The money withdrawal thread executes wait operation and wait operation-- i=6
Withdraw money thread withdrew money: 700.0 -- i=7
Money withdrawal thread--withdrawal--execution--i=7
The money withdrawal thread starts to execute the wait operation and execute the wait operation-- i=8
The money saving thread executes wait operation-- i=6
Saving thread deposit: 700.0 -- i=7
Therefore, we need to pay attention to the following points:
1. After the wait() method is executed, the current thread immediately enters the waiting blocking state, and the subsequent code will not be executed;
2. After the notify()/notifyAll() method is executed, the thread object (any-notify()/all-notifyAll()) on this synchronization lock object will be awakened. However, the synchronization lock object is not released at this time. That is to say, if there is code behind notify()/notifyAll(), it will continue to proceed. Only when the current thread is executed, the synchronization lock object will be released;
3. After notify()/notifyAll() is executed, if there is a sleep() method on the right, the current thread will enter a blocking state, but the synchronization object lock is not released and it is still retained by itself. Then the thread will continue to be executed after a certain period of time, the next 2;
4. wait()/notify()/nitifyAll() completes communication or collaboration between threads based on different object locks. Therefore, if it is a different synchronization object lock, it will lose its meaning. At the same time, the synchronization object lock is best to maintain a one-to-one correspondence with the shared resource object;
5. When the wait thread wakes up and executes, the wait() method code that was executed last time continues to be executed.
Of course, the above example is relatively simple, just to simply use the wait()/notify()/noitifyAll() method, but in essence, it is already a simple producer-consumer model.
Series of articles:
Explanation of Java multi-threaded instances (I)
Detailed explanation of Java multi-threaded instances (II)
Detailed explanation of Java multi-threaded instances (III)