通訊的方式
要想實現多個執行緒之間的協同,如:執行緒執行先後順序、獲取某個執行緒執行的結果等等。涉及到執行緒之間的相互通訊,分為下面四類:
- 檔案共享
- 網路共享
- 共享變數
- JDK提供的執行緒協調API
- suspend/resume、wait/notify、park/unpark
檔案共享
public class MainTest {
public static void main(String[] args) {
// 執行緒1 - 寫入資料
new Thread(() -> {
try {
while (true) {
Files.write(Paths.get("test.log"),
content = "當前時間" + String.valueOf(System.currentTimeMillis()));
Thread.sleep(1000L);
}
} catch (Exception e) {
e.printStackTrace();
}
}).start();
// 執行緒2 - 讀取資料
new Thread(() -> {
try {
while (true) {
Thread.sleep(1000L);
byte[] allBytes = Files.readAllBytes(Paths.get("test.log"));
System.out.println(new String(allBytes));
}
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
複製程式碼
變數共享
public class MainTest {
// 共享變數
public static String content = "空";
public static void main(String[] args) {
// 執行緒1 - 寫入資料
new Thread(() -> {
try {
while (true) {
content = "當前時間" + String.valueOf(System.currentTimeMillis());
Thread.sleep(1000L);
}
} catch (Exception e) {
e.printStackTrace();
}
}).start();
// 執行緒2 - 讀取資料
new Thread(() -> {
try {
while (true) {
Thread.sleep(1000L);
System.out.println(content);
}
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
複製程式碼
網路共享
執行緒協作-JDK API
JDK中對於需要多執行緒協作完成某一任務的場景,提供了對應API支援。
多執行緒協作的典型場景是:生產者-消費者模型。(執行緒阻塞、執行緒喚醒)
示例:執行緒1去買包子,沒有包子,則不再執行。執行緒2生產出包子,通知執行緒-1繼續執行。
API-被棄用的suspend和resume
作用:呼叫suspend掛起目標執行緒,通過resume可以恢復執行緒執行。
/** 包子店 */
public static Object baozidian = null;
/** 正常的suspend/resume */
public void suspendResumeTest() throws Exception {
// 啟動執行緒
Thread consumerThread = new Thread(() -> {
if (baozidian == null) { // 如果沒包子,則進入等待
System.out.println("1、進入等待");
Thread.currentThread().suspend();
}
System.out.println("2、買到包子,回家");
});
consumerThread.start();
// 3秒之後,生產一個包子
Thread.sleep(3000L);
baozidian = new Object();
consumerThread.resume();
System.out.println("3、通知消費者");
}
複製程式碼
被棄用的主要原因是,容易寫出不死鎖的程式碼。所以用wait/notify和park/unpark機制對它進行替代
suspend和resume死鎖示例
1、同步程式碼中使用
/** 死鎖的suspend/resume。 suspend並不會像wait一樣釋放鎖,故此容易寫出死鎖程式碼 */
public void suspendResumeDeadLockTest() throws Exception {
// 啟動執行緒
Thread consumerThread = new Thread(() -> {
if (baozidian == null) { // 如果沒包子,則進入等待
System.out.println("1、進入等待");
// 當前執行緒拿到鎖,然後掛起
synchronized (this) {
Thread.currentThread().suspend();
}
}
System.out.println("2、買到包子,回家");
});
consumerThread.start();
// 3秒之後,生產一個包子
Thread.sleep(3000L);
baozidian = new Object();
// 爭取到鎖以後,再恢復consumerThread
synchronized (this) {
consumerThread.resume();
}
System.out.println("3、通知消費者");
}
複製程式碼
2、suspend比resume後執行
/** 導致程式永久掛起的suspend/resume */
public void suspendResumeDeadLockTest2() throws Exception {
// 啟動執行緒
Thread consumerThread = new Thread(() -> {
if (baozidian == null) {
System.out.println("1、沒包子,進入等待");
try { // 為這個執行緒加上一點延時
Thread.sleep(5000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 這裡的掛起執行在resume後面
Thread.currentThread().suspend();
}
System.out.println("2、買到包子,回家");
});
consumerThread.start();
// 3秒之後,生產一個包子
Thread.sleep(3000L);
baozidian = new Object();
consumerThread.resume();
System.out.println("3、通知消費者");
consumerThread.join();
}
複製程式碼
wait/notify機制
這些方法只能由同一物件鎖的持有者執行緒呼叫,也就是寫在同步塊裡面,否則會丟擲IllegalMonitorStateException異常。 wait方法導致當前執行緒等待,加入該物件的等待集合中,並且放棄當前持有的物件鎖。 notify/notifyAll方法喚醒一個或所有正在等待這個物件鎖的執行緒。 注意:雖然會wait自動解鎖,但是對順序有要求,如果在notify被呼叫之後,才開始wait方法的呼叫,執行緒會永遠處於WAITING狀態。
wait/notify程式碼示例
/** 正常的wait/notify */
public void waitNotifyTest() throws Exception {
// 啟動執行緒
new Thread(() -> {
synchronized (this) {
while (baozidian == null) { // 如果沒包子,則進入等待
try {
System.out.println("1、進入等待");
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
System.out.println("2、買到包子,回家");
}).start();
// 3秒之後,生產一個包子
Thread.sleep(3000L);
baozidian = new Object();
synchronized (this) {
this.notifyAll();
System.out.println("3、通知消費者");
}
}
複製程式碼
造成死鎖的示例
/** 會導致程式永久等待的wait/notify */
public void waitNotifyDeadLockTest() throws Exception {
// 啟動執行緒
new Thread(() -> {
if (baozidian == null) { // 如果沒包子,則進入等待
try {
Thread.sleep(5000L);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
synchronized (this) {
try {
System.out.println("1、進入等待");
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
System.out.println("2、買到包子,回家");
}).start();
// 3秒之後,生產一個包子
Thread.sleep(3000L);
baozidian = new Object();
synchronized (this) {
this.notifyAll();
System.out.println("3、通知消費者");
}
}
複製程式碼
park/unpark機制
執行緒呼叫park則等待“許可”,unpark方法為指定執行緒提供“許可(permit)” 不要求park和unpark方法的呼叫順序。 多次呼叫unpark之後,再呼叫park,執行緒會直接執行。 但不會疊加,也就是說,連續多次呼叫park方法,第一次會拿到"許可"直接執行,後續呼叫會進入等待。
/** 正常的park/unpark */
public void parkUnparkTest() throws Exception {
// 啟動執行緒
Thread consumerThread = new Thread(() -> {
while (baozidian == null) { // 如果沒包子,則進入等待
System.out.println("1、進入等待");
LockSupport.park();
}
System.out.println("2、買到包子,回家");
});
consumerThread.start();
// 3秒之後,生產一個包子
Thread.sleep(3000L);
baozidian = new Object();
LockSupport.unpark(consumerThread);
System.out.println("3、通知消費者");
}
複製程式碼
造成死鎖的示例
/** 死鎖的park/unpark */
public void parkUnparkDeadLockTest() throws Exception {
// 啟動執行緒
Thread consumerThread = new Thread(() -> {
if (baozidian == null) { // 如果沒包子,則進入等待
System.out.println("1、進入等待");
// 當前執行緒拿到鎖,然後掛起
synchronized (this) {
LockSupport.park();
}
}
System.out.println("2、買到包子,回家");
});
consumerThread.start();
// 3秒之後,生產一個包子
Thread.sleep(3000L);
baozidian = new Object();
// 爭取到鎖以後,再恢復consumerThread
synchronized (this) {
LockSupport.unpark(consumerThread);
}
System.out.println("3、通知消費者");
}
複製程式碼
偽喚醒
警告!之前程式碼中用if語句來判斷,是否進入等待狀態,是錯誤的! 官方建議應該迴圈中檢查等待條件,原因是處於等待狀態的執行緒可能會收到錯誤警報和偽喚醒,如果不在迴圈中檢查等待條件,程式就會在沒有滿足結束條件的情況下退出。
偽喚醒是指執行緒並非因為notify、notifyall、unpark等api呼叫而喚醒,是更底層原因導致的。