【多線程】3.Java多線程環境中線程間的通信

文檔大綱

等待/通知機制

生活舉例

  1. 廚師通過傳菜鈴通知服務員上菜
  2. 出租車等待乘客呼叫

不通過等待/通知機制的實現方式

在沒有等待、通知機制的時候,我們會使用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)方法

等待某一段時間內是否有線程對鎖進行喚醒,如果超過這個時間則自動喚醒。

特殊的場景

  1. 通知過早,在線程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的清理工作

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章