1. 程式人生 > 其它 >JAVA篇:Java 多執行緒 (一) 執行緒控制

JAVA篇:Java 多執行緒 (一) 執行緒控制

1、執行緒控制

關鍵字:wait/notify/notifyAll、join、sleep、interrupt

執行緒控制討論執行緒在呼叫了start()到執行完成中間階段的行為,包含

  1. 執行緒阻塞和喚醒、超時等待

  2. 執行緒中斷機制

1.1 執行緒阻塞和喚醒、超時等待

主要討論join(),wait()、notify()和notifyAll(),以及yield()和sleep()。以及期間cpu資源及鎖資源的情況,這裡的鎖僅僅考慮synchronized(Object)物件鎖。

1.1.1 join() 方法

join()是執行緒的例項方法,有兩種形式,thread.join()和tread.join(long timeout)。join方法會阻塞當前執行緒,等待指定執行緒執行完畢後才會被喚醒,或者如果設定了超時,等到超時後當前執行緒也會被喚醒

join()方法使得當前執行緒休眠,釋放cpu資源,但是並不會釋放鎖

有說法說“其底層實現是wait()方法,會釋放鎖。”然後我寫了一個測試程式碼,形成了死鎖。wait()方法釋放鎖的相關討論在後文,在這裡先討論join()方法執行過程中的情況。

join()的字首是執行緒例項。如果要描述得清楚些則需要做一些假設。譬如說由執行緒A和t1,t1處於執行狀態,在當前執行緒A呼叫t1.join()。那麼執行緒A會無限阻塞,直到t1執行結束。

那麼A在呼叫了t1.join()後等待t1的期間是否會釋放資源呢?我感覺釋放資源這個要往細了說,釋放什麼資源?釋放cpu資源,釋放cpu資源和全部鎖資源,釋放cpu資源和指定鎖資源。

有說法join()方法呼叫之後會釋放鎖,總不可能是釋放t1持有的鎖吧,所以只能理解為釋放當前執行緒A持有的鎖。但是join方法不同,它是由t1呼叫的,也無法預見t1會需要什麼鎖資源,那麼A呼叫t1.join()只可能是任意釋放一個鎖,或者說更加靠譜地釋放全部鎖。

我按照這個思路寫了測試程式碼,但是主執行緒呼叫了join()並未釋放任何鎖,然後兩個子執行緒都無法獲得鎖,造成了死鎖。

為了防止死鎖,我將join()方法設定了超時,使得程式碼可以執行,最後的程式碼和結果如下:

 
 /* 測試join() */
    public void test2(){
        /* 模擬共享資源 
*/ Object res = new Object(); Object res2 = new Object(); SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS"); ​ /*建立子執行緒1,共享資源res*/ Runnable r1 = new Runnable() { @Override public void run() { try { Thread.sleep(3000); System.out.println(df.format(new Date())+" part11:子執行緒1休眠結束,嘗試請求res鎖"); synchronized (res){ System.out.println(df.format(new Date())+" part12:子執行緒1獲得res鎖,後進入休眠"); Thread.sleep(3000); System.out.println(df.format(new Date())+" part13:子執行緒1結束休眠,並釋放res鎖"); } } catch (InterruptedException e) { e.printStackTrace(); } } }; ​ Thread t1 = new Thread(r1); ​ t1.start(); ​ /*建立子執行緒2,共享資源res2*/ Runnable r2 = new Runnable() { @Override public void run() { try { Thread.sleep(3000); System.out.println(df.format(new Date())+" part21:子執行緒2休眠結束,嘗試請求res2鎖"); synchronized (res2){ System.out.println(df.format(new Date())+" part22:子執行緒2獲得res2鎖,後進入休眠"); Thread.sleep(3000); System.out.println(df.format(new Date())+" part23:子執行緒2結束休眠,並釋放res2鎖"); } } catch (InterruptedException e) { e.printStackTrace(); } } }; ​ Thread t2 = new Thread(r2); ​ t2.start(); ​ /* 主執行緒持有鎖,然後呼叫join */ synchronized (res2){ System.out.println(df.format(new Date())+" part01:主執行緒持有res2鎖"); synchronized (res){ System.out.println(df.format(new Date())+" part02:主執行緒持有res鎖"); System.out.println(df.format(new Date())+" part03:主執行緒呼叫t1,t2的join()"); try { t1.join(10000);//有等待時限的join t2.join(10000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(df.format(new Date())+" part04:主執行緒退出join,不再等待,釋放鎖res"); ​ } System.out.println(df.format(new Date())+" part05:主執行緒釋放鎖res2"); } ​ System.out.println(df.format(new Date())+" 測試結束。"); }

結果如下,可以看到主執行緒呼叫了join方法後既沒有釋放res,也沒有釋放res2。是等待join()超時後,主執行緒釋放了鎖,兩個子執行緒才能請求到鎖繼續執行。

2021-10-08 16:17:21:549 part01:主執行緒持有res2鎖
2021-10-08 16:17:21:549 part02:主執行緒持有res鎖
2021-10-08 16:17:21:550 part03:主執行緒呼叫t1,t2的join()
2021-10-08 16:17:24:556 part21:子執行緒2休眠結束,嘗試請求res2鎖
2021-10-08 16:17:24:556 part11:子執行緒1休眠結束,嘗試請求res鎖
2021-10-08 16:17:41:553 part04:主執行緒退出join,不再等待,釋放鎖res ##子執行緒沒有獲得鎖無法執行,直到主執行緒join超時退出
2021-10-08 16:17:41:553 part12:子執行緒1獲得res鎖,後進入休眠
2021-10-08 16:17:41:553 part05:主執行緒釋放鎖res2
2021-10-08 16:17:41:553 測試結束。
2021-10-08 16:17:41:553 part22:子執行緒2獲得res2鎖,後進入休眠
2021-10-08 16:17:44:563 part23:子執行緒2結束休眠,並釋放res2鎖
2021-10-08 16:17:44:563 part13:子執行緒1結束休眠,並釋放res鎖

1.1.2 wait()、notify()和notifyAll()

wait()和notify()用於執行緒間的休眠與喚醒。wait(long timeout)可以設定超時,notifyAll()用於喚醒全部wait()狀態中的執行緒。

wait()和notify()都是定義在Object的方法,因為可以認為任意一個Object都是一種資源(或者資源的一個代表)。無論是對資源加鎖,對資源等待,喚醒等待該資源的其他物件,都是針對資源Object來進行的。

wait()和notify()需要配合synchronized使用,同一時間一個鎖只能被一個執行緒持有,當持有該物件的鎖的時候才可以呼叫該物件的wait()和notify()。當呼叫wait()的時候,當前執行緒會放棄指定物件的鎖,而notify()不會。

   /* 測試:wait()和notify() */
    public void test1(){
        /* 模擬共享資源 */
        Object res = new Object();
        Object res2 = new Object();
        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");
​
        /* 步驟1:建立子執行緒,呼叫wait() */
        Runnable r1 = new Runnable() {
            @Override
            public void run() {
               synchronized (res2){
                    synchronized (res){
                        System.out.println(df.format(new Date())+" part11:子執行緒1獲得res鎖,呼叫wait()");
                        try {
                            res.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(df.format(new Date())+" part12:子執行緒1被喚醒,並sleep-10ms");
                        try {
                            Thread.sleep(10);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(df.format(new Date())+" part13:子執行緒1釋放res鎖");
                    }
                   System.out.println(df.format(new Date())+" part14:子執行緒1釋放res2鎖");
               }
            }
        };
        Thread t1 = new Thread(r1);
        t1.start();
​
        /* 步驟2:建立與子執行緒1競爭res2的子執行緒2 */
        Runnable r2 = new Runnable() {
            @Override
            public void run() {
                synchronized (res2){
                    System.out.println(df.format(new Date())+" part21:子執行緒2獲得res2鎖,然後釋放");
                }
            }
        };
        Thread t2 = new Thread(r2);
        t2.start();
​
        /* 步驟3:主執行緒請求鎖,並呼叫notify() */
        try {
            System.out.println(df.format(new Date())+" part01:主執行緒休眠10ms");
            Thread.sleep(10);
​
            synchronized (res){
                System.out.println(df.format(new Date())+" part02:主執行緒獲得res鎖,並呼叫notify()");
                res.notify();
                System.out.println(df.format(new Date())+" part03:主執行緒休眠10ms");
                Thread.sleep(10);
                System.out.println(df.format(new Date())+" part04:主執行緒釋放res鎖");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        /* 步驟4:主執行緒請求鎖 */
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(df.format(new Date())+" part05:主執行緒請求res鎖");
        synchronized (res){
            System.out.println(df.format(new Date())+" part06:主執行緒獲得res鎖");
            System.out.println(df.format(new Date())+" part07:主執行緒釋放res鎖");
        }
        System.out.println("測試結束。");
​
    }

結果如下:

2021-10-08 15:13:54:679 part11:子執行緒1獲得res鎖,呼叫wait()
2021-10-08 15:13:54:680 part01:主執行緒休眠10ms
2021-10-08 15:13:54:697 part02:主執行緒獲得res鎖,並呼叫notify()
2021-10-08 15:13:54:697 part03:主執行緒休眠10ms
2021-10-08 15:13:54:712 part04:主執行緒釋放res鎖
2021-10-08 15:13:54:712 part12:子執行緒1被喚醒,並sleep-10ms
2021-10-08 15:13:54:714 part05:主執行緒請求res鎖
2021-10-08 15:13:54:723 part13:子執行緒1釋放res鎖
2021-10-08 15:13:54:723 part14:子執行緒1釋放res2鎖
2021-10-08 15:13:54:723 part06:主執行緒獲得res鎖
2021-10-08 15:13:54:723 part07:主執行緒釋放res鎖
測試結束。
2021-10-08 15:13:54:723 part21:子執行緒2獲得res2鎖,然後釋放

測試程式碼執行邏輯如下:

  • part01:主執行緒建立子執行緒t1後休眠

  • part11:子執行緒t1獲得res鎖,並且呼叫wait()後釋放res鎖,進入休眠

  • part02:主執行緒休眠結束,請求res鎖,由於此時並沒有執行緒佔有res鎖,主執行緒獲得鎖,並且呼叫notify()喚醒等待的子執行緒t1。但是雖然t1被喚醒,由於並未獲得res鎖,無法運行同步程式碼區裡面的程式碼,仍處於請求等待階段

  • part03:主執行緒在喚醒t1後,在仍佔有res鎖的情況下自顧自休眠sleep了,子執行緒t1仍在請求鎖並等待

  • part04:主執行緒睡醒了,釋放res鎖

  • part12:子執行緒t1獲得res鎖,然後休眠sleep

  • part05:主執行緒又需要請求res鎖,但是res鎖被子執行緒佔有,所以主執行緒等待

  • part13:子執行緒t1睡醒了,釋放res鎖

  • part06:主執行緒獲得res鎖,進入同步方法區

  • part07:主執行緒釋放res鎖

  • part21:子執行緒2主要用來測試res.wait()方法呼叫後是否會釋放res2鎖,顯然是不會的。子執行緒1即使處於res.wait()阻塞狀態,仍然持有res2資源鎖。

1.1.3 鎖

這裡討論的鎖由synchronized所修飾,只有獲得鎖,才能進入指定的方法區執行方法。一個鎖在同一時間只能被一個執行緒佔有,其他需要申請鎖的執行緒則會被阻塞。

針對共享資源res,所涉及的問題包含:

  1. synchronized(res)對該資源進行同步,只有獲得res鎖才可以進入同步方法區。

  2. 獲得了res鎖之後才可以呼叫res的wait()方法,呼叫wait()方法後當前執行緒休眠,並釋放之前佔有的res鎖,但是不會釋放其他資源的鎖。

  3. 獲得了res鎖之後才可以呼叫res的notify()方法,在等待該資源的全部執行緒中任意喚醒一個執行緒。但是被喚醒執行緒不佔有res鎖,如果需要進入同步方法區,需要重新競爭res鎖。

  4. notifyAll()方法則是喚醒等待該資源的全部執行緒,所有執行緒進入競爭res鎖的狀態。

1.1.4 wait()、sleep()、yield()、join()

Object.wait()、Thread.sleep(long timeout)、Thread.yield()、thread.join()的區別需要提及執行緒執行過程中的執行狀態、就緒狀態和阻塞狀態。

就緒狀態的執行緒只需要等待cpu資源就可以進入執行狀態,阻塞狀態在等待某些條件滿足後才能進入就緒狀態,等待進入執行狀態。

阻塞的情況分三種:

  • 等待阻塞:執行的執行緒執行wait()方法,該執行緒會釋放佔用的所有資源,JVM會把該執行緒放入等待池中。進入這個狀態後是不能自動喚醒的,必須依靠其他執行緒呼叫notify()/notifyAll()方法才能被喚醒。

  • 同步阻塞:執行的執行緒在獲取物件的(synchronized)同步鎖時,若該同步鎖被其他執行緒佔用,則JVM會把該執行緒放入“鎖池”中。

  • 其他阻塞:通過呼叫執行緒的sleep()或者join()或發出了I/O請求時,執行緒會進入到阻塞狀態。當sleep()狀態超時、Join()等待執行緒終止或者超時、或者I/O處理完畢時,執行緒重新回到就緒狀態。

而Object.wait()、Thread.sleep()、Thread.yield()這三種方法。

  1. Thread.yield()屬於讓執行緒從執行狀態回到就緒狀態,暫時讓出cpu資源,與其他就緒狀態執行緒一起等待cpu資源,也有可能會馬上回到執行狀態。

  2. Thread.sleep()屬於強制執行緒休眠,只是讓出了cpu的執行權,並不會釋放同步資源鎖,等到休眠時間超時後會重新進入就緒狀態。

  3. Object.wait()則分為帶時間或者不帶時間兩種休眠,會釋放資源,等時間超時或者呼叫notify()/notifyAll()方法後需要重新申請所需的鎖資源,仍處於阻塞狀態,需要等待獲取了鎖才能夠進入就緒狀態。

  4. thread.join()方法會阻塞當前執行緒,等待指定執行緒執行完畢後才會被喚醒。有說法說“其底層實現是wait()方法,會釋放資源。”然後我寫了一個測試程式碼,形成了死鎖。這個的具體描述在下面。

1.2 執行緒中斷機制

1.2.1 中斷機制

Java沒有提供一種安全、直接的方法來停止某個執行緒,而是提供了中斷機制。中斷機制中每個執行緒物件都有一箇中斷標識位表示是否有中斷請求,想要發出中斷請求的執行緒只能將指定執行緒的中斷標識位設定為True,指定執行緒可以考慮是否要檢查這個標識位並且做出反應。換言之,如果我這個執行緒從頭到尾沒有檢查標識位的行為,其他執行緒將這個中斷標識位設定為True也是完全沒有用的,其他執行緒並不能直接中斷這個執行緒。

Thread提供了三個方法:

  1. interrupt()方法,這是Thread類的例項方法,對一個執行緒呼叫interrupt()方法表示請求終端這個執行緒。該方法是唯一能將中斷狀態設定為True的方法。

  2. isInterrupted()方法,這是Thread類的例項方法,測試執行緒是否已經終端,也就是測試執行緒中中斷狀態是否被設定為True,也即是是否有中斷請求。

  3. Thread.interrupted(),這是Thread類的靜態方法,判斷執行緒是否被中斷並清除中斷狀態,即先判斷是否有中斷請求返回True/False,然後將中斷標識位重置為False。

1.2.2 中斷請求測試程式碼

下面是一個執行緒響應中斷請求的測試程式碼。

 /* 中斷機制測試 */
    public void test3()
    {
        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");
        Runnable r1 = new Runnable() {
            @Override
            public void run() {
                System.out.println(df.format(new Date())+" part11:子執行緒1開始執行,需要中斷5次才能退出!");
                int inter_times = 5;
​
                while (true){
                    //if(Thread.currentThread().isInterrupted()){//這個判斷後不會將中斷標識位重新置為False,所以主執行緒中斷一次就會觸發5次反應
                    if(Thread.interrupted()){
                        inter_times = inter_times-1;
                        System.out.println(df.format(new Date())+" part13:子執行緒1被中斷一次,剩餘次數:"+inter_times);
                        if(inter_times<=0){
                            System.out.println(df.format(new Date())+" part14:子執行緒1被成功中斷,退出執行");
                            return;
                        }
                    }else{
                        System.out.println(df.format(new Date())+" part12:子執行緒1仍在執行");
                    }
                }
​
​
            }
        };
​
        Thread t1 = new Thread(r1);
        t1.setDaemon(true);
        t1.start();
​
​
        while (t1.isAlive()){
            System.out.println(df.format(new Date())+" part01:主執行緒嘗試中斷子執行緒1.....");
            t1.interrupt();
​
            /*try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }*/
​
        }
​
        System.out.println(df.format(new Date())+"測試結束。");
​
    }

結果如下:

2021-10-09 14:37:02:039 part11:子執行緒1開始執行,需要中斷5次才能退出!
2021-10-09 14:37:02:039 part01:主執行緒嘗試中斷子執行緒1.....
2021-10-09 14:37:02:040 part12:子執行緒1仍在執行
2021-10-09 14:37:02:040 part01:主執行緒嘗試中斷子執行緒1.....
2021-10-09 14:37:02:040 part13:子執行緒1被中斷一次,剩餘次數:4
2021-10-09 14:37:02:040 part01:主執行緒嘗試中斷子執行緒1.....
2021-10-09 14:37:02:040 part13:子執行緒1被中斷一次,剩餘次數:3
2021-10-09 14:37:02:040 part01:主執行緒嘗試中斷子執行緒1.....
2021-10-09 14:37:02:040 part13:子執行緒1被中斷一次,剩餘次數:2
2021-10-09 14:37:02:040 part01:主執行緒嘗試中斷子執行緒1.....
2021-10-09 14:37:02:040 part13:子執行緒1被中斷一次,剩餘次數:1
2021-10-09 14:37:02:040 part01:主執行緒嘗試中斷子執行緒1.....
2021-10-09 14:37:02:040 part13:子執行緒1被中斷一次,剩餘次數:0
2021-10-09 14:37:02:040 part01:主執行緒嘗試中斷子執行緒1.....
2021-10-09 14:37:02:040 part14:子執行緒1被成功中斷,退出執行
2021-10-09 14:37:02:040 part01:主執行緒嘗試中斷子執行緒1.....
2021-10-09 14:37:02:041測試結束。

1.2.3 執行緒所處狀態以及中斷

執行狀態的執行緒,再接收到中斷請求(isInterrupted()\Thread.interrupted())之後,可以選擇在合適的位置對中斷請求做出對應反應或者說不做反應。

但是有一部分方法,如sleep、wait、notify、join,這些方法會丟擲InterruptedException,當遇到了中斷請求,必須有對應的措施,可以在catch塊中進行處理,也可以拋給上一次層。因為Java虛擬機器在實現這些方法的時候,本身就有某種機制在判斷中斷標識位,如果中斷了,就丟擲一個InterruptedException。

1.X 參考

Java多執行緒8:wait()和notify()/notifyAll()

多執行緒面試題之為什麼wait(),notify(),notifyAll()等方法都是定義在Object類中

【Java併發系列02】Object的wait()、notify()、notifyAll()方法使用

4.sleep()和wait()方法有什麼區別?

Java執行緒的wait(), notify()和notifyAll()

阻塞和喚醒執行緒——LockSupport功能簡介及原理淺析

Java執行緒狀態以及 sheep()、wait()、yield() 的區別

Java多執行緒17:中斷機制

JAVA中斷機制

當你深入瞭解,你就會發現世界如此廣袤,而你對世界的瞭解則是如此淺薄,請永遠保持謙卑的態度。