等待/通知機制
生活舉例
- 廚師通過傳菜鈴通知服務員上菜
- 出租車等待乘客呼叫
不通過等待/通知機制的實現方式
在沒有等待、通知機制的時候,我們會使用while循環來輪詢希望的條件是否滿足,例如:
// ThreadA
public class ThreadA extends Thread {
private List<String> list;
public ThreadA(List<String> list) {
this.list = list;
}
@Override
public void run() {
try {
for (int i = 0; i < 10; i++) {
list.add("ThreadA");
System.out.println("add A " + i);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// ThreadB
public class ThreadB extends Thread {
private volatile List<String> list;
public ThreadB(List<String> list) {
this.list = list;
}
@Override
public void run() {
try {
while (true) {
if (list.size() == 5) {
System.out.println("size = 5 , threadB exit.");
throw new InterruptedException();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 執行入口
public class Test {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
ThreadA threadA = new ThreadA(list);
threadA.start();
ThreadB threadB = new ThreadB(list);
threadB.start();
}
}
可以看到我們使用while(true)和volatile關鍵字來實現實時感知某個條件的變化,但是帶來的缺點就是ThreadB是一直在運行,一直消耗CPU資源;如果輪詢間隔時間比較短,則很浪費CPU資源;如果輪詢時間間隔很長,則可能錯過數據變化的感知時機。
等待/通知機制的實現
wait()和notify()/notifyAll()方法
方法wait()
的作用是使當前執行代碼的線程進行等待,wait()
方法是Object
類的方法,該方法用來將當前線程置入“預執行隊列”中,並且再wait()
所在的代碼行處停止執行,知道接到通知或被中斷爲止。在調用wait()
之前,線程必須獲得該對象的對象級別鎖,即只能在同步方法或同步塊中調用wait()
方法。在執行wait()
方法後,當前線程釋放鎖。在從wait()
返回前,線程與其他線程競爭重新獲得鎖。如果調用wait()
時沒有持有適當的鎖,則拋出IllegalMonitorStateException
。
方法notify()
會隨機喚醒一個線程,它也要在同步方法或同步塊中調用,即在調用前,線程也必須獲得該對象的對象級別鎖。如果調用notify()
時沒有持有適當的鎖,也會拋出IllegalMonitorStateException
。該方法用來通知哪些可能等待該對象的對象鎖的其他線程,如果有多個線程等待,則由線程規劃器隨機挑選出其中一個呈wait
狀態的線程,對其發出通知notify
,並使他獲得該對象的對象鎖。在執行notify()
方法後,當前線程不會馬上釋放該對象鎖,呈wait
狀態的線程也並不能馬上獲得該對象鎖,要等到執行notify()
方法的線程將程序執行完,也就是退出synchronized
代碼塊後,當前線程纔會釋放鎖。線程執行wait()
後會釋放該對象鎖,一直繼續阻塞在wait
狀態,直到這個對象發出一個notify
或者notifyAll
(喚醒所有線程)。
總結:wait
使線程釋放對象鎖、停止運行,而notify
使停止的線程繼續運行。
wait線程被打斷的時候
當因爲執行了wait()
方法在等待的線程,被調用interrupt
方法打斷時候,會拋出InterruptionException
wait(long)方法
等待某一段時間內是否有線程對鎖進行喚醒,如果超過這個時間則自動喚醒。
特殊的場景
- 通知過早,在線程
wait()
之前發送的notify()
並不會對後來的等待線程起效,因爲那個時候線程還沒有進入等待狀態
管道通信
一個線程發送數據到輸出管道,另一個線程從輸入管道中讀數據。
-
字節流(管道傳遞byte):PipedInputStream 和 PipedOutputStream
-
字符流(管道傳遞字符):PipedReader 和 PipedWriter
核心連接代碼:
PipedInputStream inputStream = new PipedInputStream(); PipedOutputStream outputStream = new PipedOutputStream(); outputStream.connect(inputStream);
核心的輸入輸出代碼:
outputStream.write("some data."); byte[] byteArr = new byte[20]; inputStream.read(byteArr); outputStream.close(); inputStread.close();
只要輸出管道不執行關閉,當沒有數據的時候,輸入管道將會阻塞等待,等待數據的繼續寫入,除非是輸出管道關閉之後,則inputStream會返回-1標識沒有更多的數據了。如果返回-1繼續調用
read
方法,則會拋出IOException
join()方法的使用
主線程調起子線程,希望等待子線程執行完成之後再結束,比如子線程處理一個數據,主線程要取得這個值,就需要使用join()
方法
方法join()/join(long)
的底層是使用wait的方法來實現,所以join()執行會導致鎖的釋放,當再次喚醒的時候需要再次爭搶鎖,與Thread.sleep(long)
方法不同,Thread.sleep(long)
不釋放鎖。
join()被打斷
當一個join()的線程(其實也就是wait()的線程)被interrupt()
等打斷的時候,同樣會拋出InterruptionException
ThreadLocal類的使用
ThreadLocal類
主要解決的就是每個線程綁定自己的值,存儲每個線程的私有數據。
基本用法:
// 雖然聲明是public static 的,但是他的數據還是基於線程共享的,特指get/set方法的值,非t1這個對象
public static ThreadLocal t1 = new ThreadLocal();
public static void main(String[] args) {
t1.set("a");
t1.get();
}
給ThreadLocal一個初始值,那麼首次沒有set的時候,獲取的就不是null,而是這個初始值:
public class TheadLocalExt extends ThreadLocal {
@Overrive
protected Object initialValue() {
return "init value.";
}
}
InheritableThreadLocal類
使用InheritableThreadLocal可以在子線程中取得父線程繼承下來的值。
這裏的父子線程指:父線程指調起子線程的那個線程,子線程指被調起的那個線程。
假設子線程除了繼承父線程的ThreadLocal外,還需要執行一些操作,這時候可以這麼做:
public class InheritableThreadLocalExt extends InheritableThreadLocal {
@Overrive
protected Object initialValue() {
return "init value.";
}
@Overrive
protected Object childValue(Object parentValue) {
return parentValue + "i am child.";
}
}
關於線程池中的ThreadLocal的坑
在像SpringMVC
等使用了線程池技術,線程複用的地方,需要注意,如果上一個執行的操作修改了ThreadLocal
的內容,但是結束操作的時候沒有清除,那麼下一次的操作(SpringMVC
是下一次http請求)到來的時候,這時候從線程池拿出來的複用線程,ThreadLocal
中會殘留上一次的信息,導致意外的發生。
所以使用了線程池、線程複用的地方,需要執行操作完成後ThreadLocal
的清理工作