使用 synchronized修飾,表示該方法是加鎖的方法。使用相同this鎖的方法,在任意時刻只有一個方法會被執行,在多線程中是競爭關係。除此之外多線程還存在依賴關係。例如,一個線程須等待另一個線程返回結果後,才能繼續執行。Java中提供了相應的機制。
1、synchronized、wait、notify
考慮一個實際的流水線作業場景,一個線程負責生產產品,另一個線程負責在流水線上裝配。由於生產時間不確定,爲了不錯過傳送帶上的產品,裝配線程需要不斷檢查當前傳送帶上有無產品。爲了簡化邏輯,這裏只有一個線程負責生產,一個線程負責裝配。
import java.util.Arrays; import java.util.LinkedList; import java.util.Queue; public class ThreadDispatch { public static void main(String[] args) { var pack = new PackQueue(); // 負責每隔1秒裝入一次數據 Thread t1 = new Thread() { @Override public void run() { for (int i = 0; i < 10; i++) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } pack.addPack(Integer.toString(i)); } } }; // 每隔0.6秒鐘取出數據 Thread t2 = new Thread() { @Override public void run() { for (int i = 0; i < 20; i++) { try { Thread.sleep(600); } catch (InterruptedException e) { e.printStackTrace(); } String s = pack.getPack(); System.out.println(s); } } }; t1.start(); t2.start(); } } class PackQueue { private Queue<String> q = new LinkedList<>(); public void addPack(String s) { this.q.add(s); } public String getPack() { if (this.q.isEmpty()) { return "empty"; } return this.q.remove(); } } //empty //0 //empty //1 //2 //empty //3 //empty //4 //5 //empty //6 //empty //7 //8 //empty //9 //empty //empty //empty
以上邏輯是使用循環實現的。爲了不錯過傳送帶上的商品,裝配線程t2需不斷定時檢查。這對設置檢查的頻率提出了要求。頻率稍慢會錯過產品,頻率過快會浪費性能。最好的方式是,線程1生產好產品後,通知線程2裝配,這樣解決了“來不及”和“速度過快”的問題。
public class ThreadDispatch { public static void main(String[] args) { var pack = new PackQueue(); // 負責每隔1秒裝入一次數據 Thread t1 = new Thread() { @Override public void run() { for (int i = 0; i < 10; i++) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } pack.addPack(Integer.toString(i)); } } }; // 檢查有無數據,沒有就等待 Thread t2 = new Thread() { @Override public void run() { try { while(true) { String s = pack.getPack(); System.out.println(s); } } catch (InterruptedException e) { e.printStackTrace(); } } }; t1.start(); t2.start(); } } class PackQueue { private Queue<String> q = new LinkedList<>(); public synchronized void addPack(String s) { this.q.add(s); this.notify(); } public synchronized String getPack() throws InterruptedException { if (this.q.isEmpty()) { // return "empty"; this.wait(); } return this.q.remove(); } } // 0 // 1 // 2 // 3 // 4 // 5 // 6 // 7 // 8 // 9
優化之後,裝配線程t2 不再“無節制”檢查工作區,而是等待t1線程通知後工作。需要注意的是,這裏只有一個裝配線程t2,如果有多個線程,需要調用this.notifyAll取代this.notify,表示喚醒所有正在等待this鎖的線程。喚醒多個線程,最終也只會有一個線程獲取this鎖,其餘線程繼續等待。另外,t2 線程在 t1 線程停止生產後永遠也醒不過來了。考慮爲 t2 線程指定一個wait超時時間,超時後會自動醒來。
2、 ReentrantLock、Condition
java5中引入了高級的處理併發的java.util.concurrent包,相比較synchronized機制,提供了嘗試獲取鎖、超時等待等更多功能;不同於synchronized 在Java語言層面實現自動釋放鎖而不必考慮異常,ReentrantLock由Java代碼實現,因此需要正確捕獲異常和釋放鎖。使用 ReentrantLock 重寫示例:
import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; class PackQueue { private Queue<String> q = new LinkedList<>(); private final Lock lock = new ReentrantLock(); private final Condition condition = lock.newCondition(); public void addPack(String s) { lock.lock(); try { this.q.add(s); condition.signalAll(); } finally { lock.unlock(); } } public String getPack() throws InterruptedException { lock.lock(); try { if (this.q.isEmpty()) { condition.await(2, TimeUnit.SECONDS); } return this.q.remove(); } finally { lock.unlock(); } } }
值得注意的是,判斷隊列爲空後,調用 condition.await(2, TimeUnit.SECONDS); 表示線程會自動超時醒來。由於此時隊列爲空,調用remove方法會報錯;不過線程可以自動喚醒了。總結一下:
synchronized |
ReentrantLock
|
|
加鎖 | 通常使用 synchronized 修飾方法,表示使用this實例加鎖 |
private final Lock lock = new ReentrantLock(); lock.lock() |
釋放鎖 | 自動釋放 |
lock.unlock(); |
線程等待 |
this.wait();
|
private final Condition condition = lock.newCondition(); condition.await(); |
線程喚醒 |
this.notify(); this.notifyAll(); |
condition. signal();
condition.signalAll();
|
鎖類型 | 可重入鎖 | 可重入鎖 |
嘗試獲取鎖 | 不支持 |
lock.tryLock(1, TimeUnit.SECONDS) |
超時自動喚醒 |
不支持 |
condition.await(2, TimeUnit.SECONDS); |