【Java多線程學習筆記三】線程間通信

wallhaven-mp9931.jpg


3. 線程間通信

技術點:

  • 使用 wait/notify 實現線程間的通信
  • 生產者/消費者模式的實現
  • 方法 join 的使用
  • ThreadLocal 類的使用
3.1 等待/通知機制
3.1.1 不使用等待/通知機制實現線程間通信

通過 while(true) 不斷輪詢,實現線程間通信。

缺點:如果輪詢的時間間隔很小,更浪費 CPU 資源;如果輪詢的時間間隔很大,有可能會取不到想要的數據,所以就需要有一種機制來實現減小 CPU 的資源浪費,而且還可以實現多個線程間通信,他就是【wait/notify】機制。

3.1.2 什麼是等待/通知機制

等待/通知機制在生活的例子,比如在就餐時就會出現

  1. 廚師做完一道菜的時間不確定,所以廚師將菜品放到【菜品傳遞臺】上的時間也不確定

    QQ20200529192200.png

  2. 服務員取到菜的時間取決於廚師,所以服務員就有【等待 wait】的狀態

  3. 服務員如何能取得菜呢?這又取決於廚師,廚師將菜放在菜品傳遞臺上,其實就相當於一種通知【notify】,這時服務員纔可以拿到菜並交給就餐者

  4. 這個過程就相當於【等待/通知】機制

3.1.3 等待/通知機制的實現

方法 wait()的作用是使當前執行代碼的線程進行等待,wait()方法是 Obejct 類的方法,該方法用來將當前線程置入【預執行】隊列中,並且在 wait() 所在的代碼行處停止執行,直到能夠接到通知或中斷爲止。在調用 wait() 之前,線程必須獲得該對象的對象級別鎖,即只能在同步方法或同步代碼塊中調用 wait() 方法。在執行 wait() 方法後,當前線程釋放鎖,在從 wait() 返回前,線程與其他線程競爭重新獲得鎖。如果調用 wait() 時沒有適當的鎖,則拋出 IllegalMonitorStateException,它是 RuntimeException 的一個子類,因此,不需要 try-catch 語句進行捕捉異常。

方法 notify() 也要在同步方法或者同步代碼塊中調用,即在調用前,線程也必須獲得該對象的對象級別鎖。如果調用 notify() 時沒有持有適當的鎖,也會拋出 IllegalMonitorStateException。該方法用來通知那些可能等待該對象的對象鎖的其他線程,如果有多個線程等待,則由線程規劃器隨機挑選其中一個呈 wait 狀態的線程,對其發出通知 notify,並使它等待獲取該對象的對象鎖。需要說明的是,在執行 notify() 方法後,當前線程不會馬上釋放對象鎖,要等到執行 notify() 方法的線程將程序執行完,也就是退出 synchronized 代碼塊後,當前線程纔會釋放鎖,而呈 wait狀態所在的線程纔可以獲取該對象鎖。當第一個獲得了該對象鎖的 wait 線程運行完畢後,他會釋放掉該對象鎖,此時如果該對象沒有再次使用 notify 語句,則即使該對象已經空閒,其他 wait 狀態等待的線程由於沒有得到該對象的通知,還會繼續阻塞在 wait 狀態,直到這個對象發出一個 notify 或 notifyAll。

用一句話來總結 wait 和 notify:wait 使線程停止運行,而 notify 使停止的線程繼續運行。

關鍵字 synchronized 可以將任何一個 Object 對象作爲同步對象來看待,而 Java 爲每個 Object 都實現了 wait() 和 notify() 方法,它們必須用在被 synchronized 同步的 Object 的臨界區內。同時釋放被同步對象的鎖。而 notify 操作可以喚醒一個因調用了 wait 操作而處於阻塞狀態的線程,使其進入就緒狀態。被重新喚醒的線程會試圖重新獲得臨界區的控制權,也就是鎖,並繼續執行臨界區內 wait 之後的代碼。如果發出 notify 操作時沒有處於阻塞狀態中的線程,那麼該命令會被忽略。

wait() 方法可以使調用該方法的線程釋放共享資源的鎖然後從運行狀態退出,進入等待隊列,直到再次被喚醒。

notify() 方法可以隨機喚醒等待隊列中等待同一共享資源的一個線程並使該線程退出等待隊列,進入可運行狀態,也就是 notify() 方法僅通知一個線程。

notifyAll() 方法可以使所有正在等待隊列中等待同一共享資源的全部線程從等待狀態退出,進入可運行狀態。此時,優先級最高的那個線程最先執行,但也可能是隨機執行,這要取決於 JVM 虛擬機的實現。

status.jpg

  1. 新建一個線程對象後,再調用它的 start( ) 方法,系統就會爲此線程分配 CPU 資源,使其處於 Runnable(可運行)狀態,這是一個準備運行的階段。如果線程搶佔到 CPU 資源,此線程就處於 Running(運行)狀態。

  2. Runnable 狀態和 Running 狀態可相互切換,因爲有可能線程運行一段時間後,有其他優先級高的線程搶佔了 CPU 資源,這時線程就從 Running 狀態變成 Runnable 狀態。

    線程進入 Runnable 狀態大體可分爲如下五種情況:

    • 調用 sleep( ) 方法後經過的時間超過了指定的休眠時間。
    • 線程調用的阻塞 IO 已經返回,阻塞方法執行完畢。
    • 線程成功地獲得了試圖同步的監視器。
    • 線程正在等待某個通知,其它線程發出了通知。
    • 處於掛起狀態的線程調用了 resume 恢復方法。
  3. Blocked 是阻塞的意思,例如遇到了一個 IO 操作,此時 CPU 處於空閒狀態,可能會轉而把 CPU 時間片分配給其它線程,這是也可稱爲【暫停】狀態。Blocked 狀態結束後,進入 Runnable 狀態,等待系統重新分配資源。

    出現阻塞的情況大體可分爲如下 5 種:

    • 線程調用 sleep 方法,主動放棄佔用的處理器資源。
    • 線程調用了阻塞式 IO 方法,在該方法返回前,該線程被阻塞。
    • 線程試圖獲得一個同步監視器,但該同步監視器正被其它線程所擁有。
    • 線程等待某個通知。
    • 程序調用了 suspend 方法將該線程掛起。此方法容易導致死鎖,儘量避免使用該方法。
  4. run( ) 方法運行結束後進入銷燬階段,整個線程執行完畢。

    **每個鎖對象都有兩個隊列,一個是就緒隊列,一個是阻塞隊列。就緒隊列存儲了將要獲得鎖的線程,阻塞隊列存儲了被阻塞的線程。**一個線程被喚醒後,纔會進入就緒隊列,等待 CPU 的調度;反之,一個線程被 wait 後,就會進入阻塞隊列,等待下一次被喚醒。

3.1.4 方法 wait() 鎖釋放與 notify() 鎖不釋放

當方法 wait() 被執行後,鎖被自動釋放,但執行完 notify() 方法後,鎖卻不自動釋放。

如果將 wait() 方法改爲 sleep() 方法,就成了同步的效果,因爲 sleep() 不釋放鎖。

3.1.5 當 interrupt 方法遇到 wait 方法

當線程呈 wait() 狀態時,調用線程對象的 interrupt() 方法會出現 InterruptsedException 異常。

小總結:

  • 執行完同步代碼塊就會釋放對象中的鎖。
  • 在執行同步代碼塊的過程中,遇到異常而導致線程終止,鎖也會被釋放。
  • 在執行同步代碼塊的過程中,執行了鎖所屬對象的 wait() 方法,這個線程會釋放對象所,而此線程對象會進入線程等待池中,等待被喚醒。
3.1.6 只通知一個線程

調用方法 notify() 一次只隨機通知一個線程進行喚醒。

當多次調用 notify() 方法時,會隨機將等待 wait 狀態的線程進行喚醒。

3.1.7 喚醒所有進程

notifyAll() 方法可喚醒全部線程。

3.1.8 方法 wait(long) 的使用

帶一個參數的 wait(long) 方法的功能是等待某一段時間內是否有線程對鎖進行喚醒,如果超過這個時間則自動喚醒。

3.1.9 通知過早

如果通知過早,則會打亂程序正常的運行邏輯。

如果通知在 wait 前面,則 wait 永遠不會被通知。

3.1.10 等待 wait 的條件發生變化
3.1.11 生產者/消費者模式實現

1. 一個生產者與一個消費者:操作值

現象:setValue() 與 getValue() 交替執行

package multithread.testProducerAndConsumer;


class P{
    private String lock;

    public P(String lock){
        super();
        this.lock = lock;
    }

    public void setValue(){
        try {
            synchronized (lock){
                if(!ValueObject.value.equals("")){
                    lock.wait();
                }
                String value = System.currentTimeMillis() + "_"
                        + System.nanoTime();
                System.out.println("set的值是:" + value);
                ValueObject.value = value;
                lock.notify();
            }
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

class C{
    private String lock;
    public C(String lock){
        super();
        this.lock = lock;
    }

    public void getValue(){
        try {
            synchronized (lock){
                if(ValueObject.value.equals("")){
                    lock.wait();
                }
                System.out.println("get 的值是:" + ValueObject.value);
                ValueObject.value = "";
                lock.notify();
            }
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

class ValueObject{
    public static String value = "";
}

class ThreadP extends Thread{
    private P p;

    public ThreadP(P p){
        super();
        this.p = p;
    }

    @Override
    public void run(){
        while (true){
            p.setValue();
        }
    }
}

class ThreadC extends Thread{
    private C c;

    public ThreadC(C c){
        super();
        this.c = c;
    }

    @Override
    public void run(){
        while (true){
            c.getValue();
        }
    }
}

public class Test {
    public static void main(String[] args) {
        String lock = new String("");
        P p = new P(lock);
        C c = new C(lock);
        ThreadP pThread = new ThreadP(p);
        ThreadC cThread = new ThreadC(c);
        pThread.start();
        cThread.start();
    }
}

2. 多生產與多消費:操作值 - 假死

【假死】的現象其實激素hi線程進入 WAITING 等待狀態。如果全部線程都進入了 WAITING 狀態,則程序就不再執行任何業務功能了,整個項目呈停止狀態。

從打印的信息來看,呈假死狀態的進程中所有的線程都是呈 WAITING 狀態,爲什麼會出現這樣的情況呢?在代碼中已經用了 wait/notify 了啊?

在代碼中確實已經通過 wait/notify 進行通信了,但不保證 notify 喚醒的是異類,也許是同類,比如【生產者】喚醒【生產者】,或【消費者】喚醒【消費者】這樣的情況。如果按這樣情況運行的比率積少成多,就會導致所有的線程都不能運行下去,大家都在等待,都呈 WAITING 狀態,程序最後也就呈【假死】狀態,不能繼續運行下去了。

解決辦法:將 notify() 改爲 notifyAll() 即可,它的原理就是不光通知同類線程,也包括異類,這樣就不至於出現假死的狀態了,程序就會一直運行下去。

4. 一生產與一消費:操作棧

5. 一生產與多消費 - 操作棧:解決 wait 條件改變與假死

使用一個生產者向堆棧對象中放入數據,而多個消費者從堆棧中取出數據。

要注意不能使用 notify 進行通知,因爲會導致【假死】狀態,要使用notifyAll。

6. 多生產者與一消費者:操作棧

3.1.12 通過管道進行線程間通信:字節流

在 Java 語言中提供了各種各樣的輸入/輸出流 Stream,其中管道流(pipeStream)是一種特殊的流,用於在不同線程間直接傳送數據。一個線程發送數據到輸出管道,另一個線程從輸出管道中讀數據。通過使用管道,實現不同線程間的通信,而無需藉助臨時文件之類的東西。

  • PipedInputStream 和 PipedOutputStream
  • PipedReader 和 PipedWriter
3.1.13 通過管道進行線程間通信:字符流

待寫…

3.1.14 實戰:等待/通知之交叉備份

交叉打印

class DBTools{
    volatile private boolean prevIsA = false;
    synchronized public void backupA(){
        try {
            while (prevIsA == true){
                wait();
            }
            for (int i=0; i<5; i++){
                System.out.println("-----");
            }
            prevIsA = true;
            notifyAll();
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }

    synchronized public void backupB(){
        try {
            while (prevIsA == false){
                wait();
            }
            for (int i=0; i<5; i++){
                System.out.println("+++++");
            }
            prevIsA = false;
            notifyAll();
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

class BackupA extends Thread{
    private DBTools dbTools;

    public BackupA(DBTools dbTools){
        super();
        this.dbTools = dbTools;
    }

    @Override
    public void run(){
        dbTools.backupA();
    }
}

class BackupB extends Thread{
    private DBTools dbTools;
    public BackupB(DBTools dbTools){
        super();
        this.dbTools = dbTools;
    }

    @Override
    public void run(){
        dbTools.backupB();
    }
}

public class Run {
    public static void main(String[] args) {
        DBTools dbTools = new DBTools();
        for (int i=0; i<20; i++){
            BackupB output = new BackupB(dbTools);
            output.start();
            BackupA input = new BackupA(dbTools);
            input.start();
        }
    }
}

3.2 方法 join 的使用
3.2.1 學習方法 join 前的鋪墊
3.2.2 用 join() 方法來解決

方法 join 的作用是使所屬的線程對象 x 正常執行 run() 方法中的任務,而使當前線程 z 進行無限期的阻塞,等待線程 x 銷燬後再繼續執行線程 z 後面的代碼。(沒被 join 的線程阻塞,等待被 join 的線程執行完畢銷燬後執行)

方法 join 具有使線程排隊運行的作用,有些類似同步的運行效果。

join 與 sychronized 的區別是:join 在內部使用 wait() 方法進行等待,而 sychronized 關鍵字使用的是【對象監視器】原理做爲同步。

3.2.3 方法 join 與異常

在 join 過程中,如果當前線程對象被中斷,則當前線程出現異常。

3.2.4 方法 join(long) 的使用

方法 join(long) 中的參數是設定等待的時間。

3.2.5 方法 join(long) 與 sleep(long) 的區別

方法 join(long) 的功能在內部是使用 wait(long) 方法來實現的,所以 join(long) 方法具有釋放鎖的特點。

當執行 wait(long) 方法後,當前線程的鎖被釋放,那麼線程就可以調用此線程中的同步方法了。

3.2.6 方法 join() 後面的代碼提前運行:出現意外
3. 3 類 ThreadLocal 的使用

變量值的共享可以使用 public static 變量的形式,所有的線程都使用同一個 public static 變量。如果想實現每一個線程都有自己的共享變量該怎麼解決呢?可以用 ThreadLocal 解決。

3.3.1 方法 get() 與 null
package multithread.testThreadLocal;

public class Run {
    public static ThreadLocal t1 = new ThreadLocal();

    public static void main(String[] args) {
        if(t1.get() == null){
            System.out.println("從未放過值");
            t1.set("我的值");
        }
        System.out.println(t1.get());
        System.out.println(t1.get());
    }
    // 結果:
    // 從未放過值
    // 我的值
    // 我的值
}

類 ThreadLocal 解決的是變量在不同線程之間的隔離性,也就是不同線程擁有的值,不同線程中的值是可以放入 ThreadLocal 類中進行保存的。

3.3.2 驗證線程變量的隔離性

每個線程都有自己的空間,每個線程的變量的值都不同。

3.3.3 解決 get() 返回 null 問題
class ThreadLocalExt extends ThreadLocal{
    @Override
    protected Object initialValue(){
        return "我是默認值,第一個 get 不再爲 null";
    }
}

public class Run{
    public static ThreadLocalExt t1 = new ThreadLocalExt();
    public static void main(String[] args){
        if(t1.get() == null){
            System.out.println("從未放過值");
            t1.set("我的值");
        }
        System.out.println(t1.get());
        System.out.println(t1.get());
    }
}
// 運行結果如下:
// 我是默認值,第一個 get 不再爲 null
// 我是默認值,第一個 get 不再爲 null

3.3.4 再次驗證線程變量的隔離性
3.4 類 Inheritable 與 ThreadLocal 的使用

使用類 InheritableThreadLocal 可以在子線程中取得父線程繼承下來的值。

3.4.1 值繼承

使用 InheritableThreadLocal 類可以讓子線程從父線程中取得值。

3.4.2 值繼承再修改
class InheritableThreadLocalExt extends InheritableThreadLocal{
    // 初始化值
    @Override
    protected Object initialValue(){
        return new Date().getTime();
    }
    
    // 修改繼承的值
    @Override
    protected Object childValue(Object parentValue){
        return parentValue + " 我在子線程加的~!";
    }
}

但要注意,如果子線程在取得值的同時,主線程將 InheritableThreadLocal 中的值進行更改,那麼子線程取到的值還是舊值。

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