線程通信的方法

線程通信的方法

程序在使用多線程執(zhí)行任務(wù)時,經(jīng)常需要線程之間協(xié)同工作。此時,我們需要了解線程通信的手段。

線程通信大致分為以下四類:

  • 文件共享
  • 網(wǎng)絡(luò)共享
  • 共享變量
  • JDK提供的線程協(xié)調(diào)API

本文主要研究第四類,如何使用JDK提供的API正確地阻塞、喚醒目標(biāo)線程。

Thread#suspend()和Thread#resume()

Thread#suspend():掛起目標(biāo)線程不再繼續(xù)執(zhí)行,直到它被resume()喚醒。

Thread#resume():如果目標(biāo)線程已被掛起,那么喚醒目標(biāo)線程并允許它繼續(xù)執(zhí)行。

舉個栗子:

public class ThreadCommunication {

    private static volatile Object steamedStuffedBun;

    // 正常的suspend/resume
    private static void suspendResume() throws InterruptedException {
        Thread thread = new Thread(new Runnable() {
            public void run() {
                while (steamedStuffedBun == null) {
                    System.out.println("沒有包子,進(jìn)入等待");
                    // 掛起當(dāng)前線程
                    Thread.currentThread().suspend();
                }
                System.out.println("買到包子,回家");
            }
        });
        thread.start();
        // 3s后生產(chǎn)一個包子
        Thread.sleep(3000);
        steamedStuffedBun = new Object();
        System.out.println("包子做好了");
        // 喚醒目標(biāo)線程
        thread.resume();
        thread.join();
    }
    
    public static void main(String[] args) throws InterruptedException {
        suspendResume();
    }    
}

運(yùn)行main()方法,控制臺輸出如下:

沒有包子,進(jìn)入等待
包子做好了
買到包子,回家

suspend()resume()幫助我們在適當(dāng)?shù)臅r候阻塞和喚醒線程,成功地模擬了顧客在包子店買包子的情景。

但是,這對API在JDK1.2之后被棄用,原因是它們特別容易形成死鎖

如果目標(biāo)線程持有監(jiān)視器鎖,在調(diào)用suspend()掛起目標(biāo)線程時并不會釋放這把鎖。此時,如果其他線程在調(diào)用目標(biāo)線程的resume()方法之前也要先獲取這把鎖,死鎖就產(chǎn)生了。

public class ThreadCommunication {

    private static volatile Object steamedStuffedBun;

    // 會死鎖的suspend/resume
    private static void suspendResumeDeadLock() throws InterruptedException {
        final Object lock = new Object();
        Thread thread = new Thread(new Runnable() {
            public void run() {
                while (steamedStuffedBun == null) {
                    System.out.println("沒有包子,進(jìn)入等待");
                    // 當(dāng)前線程拿到鎖,然后掛起
                    synchronized (lock) {
                        Thread.currentThread().suspend();
                    }
                }
                System.out.println("買到包子,回家");
            }
        });
        thread.start();
        // 3s后生產(chǎn)一個包子
        Thread.sleep(3000);
        steamedStuffedBun = new Object();
        // 爭取到鎖以后再喚醒目標(biāo)線程
        synchronized (lock) { // 此處會一直BLOCKED
            System.out.println("包子做好了");
            thread.resume();
        }
        thread.join();
    }
    
    public static void main(String[] args) throws InterruptedException {
        suspendResumeDeadLock();
    }    
}

運(yùn)行main()方法,程序無法退出,控制臺輸出如下:

沒有包子,進(jìn)入等待

喚醒目標(biāo)線程的條件已經(jīng)滿足(包子做好了),但是拿不到鎖,就無法喚醒目標(biāo)線程,程序就這樣被“凍結(jié)”。

即使在不涉及監(jiān)視器鎖的情況下使用suspend()resume(),如果調(diào)用順序不當(dāng),線程也可能永遠(yuǎn)掛起。

public class ThreadCommunication {

    private static volatile Object steamedStuffedBun;

    // 會永遠(yuǎn)掛起的suspend/resume
    private static void suspendForever() throws InterruptedException {
        Thread thread = new Thread(new Runnable() {
            public void run() {
                while (steamedStuffedBun == null) {
                    try {
                        // 睡眠一段時間,讓resume()方法先執(zhí)行
                        Thread.sleep(5000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("沒有包子,進(jìn)入等待");
                    // 掛起當(dāng)前線程
                    Thread.currentThread().suspend();
                }
                System.out.println("買到包子,回家");
            }
        });
        thread.start();
        // 3s后生產(chǎn)一個包子
        Thread.sleep(3000);
        steamedStuffedBun = new Object();
        System.out.println("包子做好了");
        // 喚醒目標(biāo)線程(此時線程還未掛起)
        thread.resume();
        thread.join();
    }
    
    public static void main(String[] args) throws InterruptedException {
        suspendForever();
    }    
}

運(yùn)行main()方法,程序無法退出,控制臺輸出如下:

包子做好了
沒有包子,進(jìn)入等待

上面的例子中,resume()suspend()之前被調(diào)用,目標(biāo)線程被掛起之后,沒有線程來調(diào)用resume()喚醒它,就會永遠(yuǎn)掛起。

既然Thread#suspend()Thread#resume()已經(jīng)被棄用,那就讓我們來看看它們的替代方法吧。

Object#wait()和Object#notify()/Object#notifyAll()

調(diào)用以下方法時,當(dāng)前線程必須是對象監(jiān)視器鎖的持有者。

Object#wait():將當(dāng)前線程放入對象監(jiān)視器鎖的等待集合,釋放對象的監(jiān)視器鎖,線程不再被操作系統(tǒng)調(diào)度,進(jìn)入等待直到被喚醒或中斷,然后再次競爭獲得對象的監(jiān)視器鎖,恢復(fù)執(zhí)行。

Object#notify():喚醒對象監(jiān)視器鎖等待集合中的任意一個線程。

Object#notifyAll():喚醒對象監(jiān)視器鎖等待集合中的所有線程。

因此,wait()/notify()只適用于線程持有鎖的情境下。

public class ThreadCommunication {

    private static volatile Object steamedStuffedBun;

    private static void waitNotify() throws InterruptedException {
        final Object lock = new Object();
        Thread thread = new Thread(new Runnable() {
            public void run() {
                while (steamedStuffedBun == null) {
                    synchronized (lock) {
                        try {
                            System.out.println("沒有包子,進(jìn)入等待");
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
                System.out.println("買到包子,回家");
            }
        });
        thread.start();
        // 3s后生產(chǎn)一個包子
        Thread.sleep(3000);
        steamedStuffedBun = new Object();
        // 通知目標(biāo)線程
        synchronized (lock) {
            System.out.println("包子做好了");
            lock.notify();
        }
        thread.join();
    }
    
    public static void main(String[] args) throws InterruptedException {
        waitNotify();
    }    
}

運(yùn)行main()方法,控制臺輸出如下:

沒有包子,進(jìn)入等待
包子做好了
買到包子,回家

wait()/notify幫助我們成功地買到了包子。

suspend()/resume()相似,wait()/notify()如果調(diào)用順序不當(dāng),也會造成線程無法被喚醒:

public class ThreadCommunication {

    private static volatile Object steamedStuffedBun;

    private static void waitForever() throws InterruptedException {
        final Object lock = new Object();
        Thread thread = new Thread(new Runnable() {
            public void run() {
                while (steamedStuffedBun == null) {
                    try {
                        // 睡眠一段時間,讓notify()先執(zhí)行
                        Thread.sleep(5000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (lock) {
                        try {
                            System.out.println("沒有包子,進(jìn)入等待");
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
                System.out.println("買到包子,回家");
            }
        });
        thread.start();
        // 3s后生產(chǎn)一個包子
        Thread.sleep(3000);
        steamedStuffedBun = new Object();
        // 通知目標(biāo)線程
        synchronized (lock) {
            System.out.println("包子做好了");
            lock.notify();
        }
        thread.join();
    }
    
    public static void main(String[] args) throws InterruptedException {
        waitForever();
    }    
}

運(yùn)行main()方法,程序無法退出,控制臺輸出如下:

包子做好了
沒有包子,進(jìn)入等待

目標(biāo)線程的notify()方法先于wait()方法被調(diào)用,導(dǎo)致wait()方法被調(diào)用后沒有線程來喚醒它,就會一直處于WAITING狀態(tài)。

LockSupport#park()和LockSupport#unpark()

wait()/notify()不同,park()/unpark()調(diào)用時線程不用獲取對象的監(jiān)視器鎖。

LockSupport#park():禁止當(dāng)前線程進(jìn)行線程調(diào)度,直到許可證可用。如果當(dāng)前許可證可用,那么消費(fèi)該許可證,本地調(diào)用立刻返回。

LockSupport#unpark():如果目標(biāo)線程許可證不可用,則為其提供許可證。否則,目標(biāo)線程的下一次park()調(diào)用不會阻塞。

可以看出,park()unpark()為線程維護(hù)了一個許可證,該許可證只有可用/不可用兩種狀態(tài)(默認(rèn)不可用)。unpark()將許可證置為可用。而park()在許可證不可用的時候阻塞,在許可證可用的時候消費(fèi)該許可證(將其置為不可用)然后立即返回。因此,park()能否從阻塞中恢復(fù)與park()/unpark()的調(diào)用順序無關(guān)。

public class ThreadCommunication {

    private static volatile Object steamedStuffedBun;

    private static void parkUnpark() throws InterruptedException {
        Thread thread = new Thread(new Runnable() {
            public void run() {
                while (steamedStuffedBun == null) {
                    System.out.println("沒有包子,進(jìn)入等待");
                    // 掛起當(dāng)前線程
                    LockSupport.park();
                }
                System.out.println("買到包子,回家");
            }
        });
        thread.start();
        // 3s后生產(chǎn)一個包子
        Thread.sleep(3000);
        steamedStuffedBun = new Object();
        System.out.println("包子做好了");
        // 喚醒目標(biāo)線程
        LockSupport.unpark(thread);
        thread.join();
    }
    
    public static void main(String[] args) throws InterruptedException {
        parkUnpark();
    }  
}

運(yùn)行main()方法,控制臺輸出如下:

沒有包子,進(jìn)入等待
包子做好了
買到包子,回家

雖然park()/unpark()沒有調(diào)用順序的要求,但是如果在調(diào)用park()時,當(dāng)前線程已經(jīng)持有了對象的監(jiān)視器鎖,park()不會釋放該鎖。此時,如果在調(diào)用unpark()前也需要獲取對象的監(jiān)視器鎖,就會死鎖。

public class ThreadCommunication {

    private static volatile Object steamedStuffedBun;

    private static void parkUnparkDeadLock() throws InterruptedException {
        final Object lock = new Object();
        Thread thread = new Thread(new Runnable() {
            public void run() {
                while (steamedStuffedBun == null) {
                    synchronized (lock) {
                        System.out.println("沒有包子,進(jìn)入等待");
                        // 掛起當(dāng)前線程,但是不會釋放鎖
                        LockSupport.park();
                    }
                }
                System.out.println("買到包子,回家");
            }
        });
        thread.start();
        // 3s后生產(chǎn)一個包子
        Thread.sleep(3000);
        steamedStuffedBun = new Object();
        // 先拿到鎖,再喚醒目標(biāo)線程
        // 但是鎖現(xiàn)在被park()線程持有,此處一直BLOCKED,死鎖了
        synchronized (lock) {
            System.out.println("包子做好了");
            LockSupport.unpark(thread);
        }
        thread.join();
    }
    
    public static void main(String[] args) throws InterruptedException {
        parkUnparkDeadLock();
    }  
}

注意wait()park()都有可能被偽喚醒,建議在循環(huán)中檢查等待/掛起條件,防止程序在不滿足結(jié)束條件的情況下退出。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容