Java多執行緒-執行緒通訊

小碼農薛堯發表於2019-09-03

通訊的方式

要想實現多個執行緒之間的協同,如:執行緒執行先後順序、獲取某個執行緒執行的結果等等。涉及到執行緒之間的相互通訊,分為下面四類:

  • 檔案共享
  • 網路共享
  • 共享變數
  • JDK提供的執行緒協調API
    • suspend/resume、wait/notify、park/unpark

檔案共享

Java多執行緒-執行緒通訊

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();
  }
}
複製程式碼

變數共享

Java多執行緒-執行緒通訊

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繼續執行。

Java多執行緒-執行緒通訊

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呼叫而喚醒,是更底層原因導致的。

相關文章