1. 程式人生 > >Java:談談控制執行緒的幾種辦法

Java:談談控制執行緒的幾種辦法

目錄

  • Java:談談控制執行緒的幾種辦法
    • join()
    • sleep()
    • 守護執行緒
      • 主要方法
      • 需要注意
    • 優先順序
    • 棄用三兄弟
      • stop()
      • resume
      • suspend
    • 中斷三兄弟
      • interrupt()
      • interrupted()
      • isInterrupted()
    • 安全終止執行緒

前文傳送門:
Java:多執行緒概述與建立方式
Java:執行緒的六種狀態及轉化

Java:談談控制執行緒的幾種辦法

控制執行緒的辦法諸多,本篇做出一部分整理總結。

join()

官方解釋簡潔明瞭:Waits for this thread to die.,很明顯,針對執行緒來說,誰呼叫,等誰死。舉個例子:當在A執行緒中呼叫B執行緒的join()方法時,A執行緒將會被阻塞,直到B執行緒執行完畢消亡才取消阻塞

join()方法具體有三個:

//等待該執行緒消亡
public final void join()
//等待該執行緒消亡,只不過最多等millis毫秒。
public final synchronized void join(long millis)
//等待該執行緒消亡,只不過最多等millis毫秒+nanos納秒(毫微秒)。
public final synchronized void join(long millis, int nanos)

簡單測試一下,理解更加深刻:

    public static void main(String[] args) throws InterruptedException {
        //建立執行緒
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    System.out.println(Thread.currentThread().getName()+":"+i);
                }
            }
        });
        //啟動執行緒
        t.start();
        for (int i = 0; i < 3; i++) {
            System.out.println(Thread.currentThread().getName()+":"+i);
            //i為1時,主執行緒阻塞,等待執行緒t執行並消亡
            if(i == 1)
                t.join();
        }
    }

需要注意的是,join的執行緒必須已經start了才行。

sleep()

sleep(long)是Thread類中的靜態方法,上一篇談到過sleep(long)方法和yield()方法的不同之處:

  • sleep(long)方法會使執行緒轉入阻塞狀態,時間到了之後才會轉入就緒狀態。而yield()方法不會將執行緒轉入阻塞狀態,而是強制執行緒進入就緒狀態。
  • 使用sleep(long)方法需要處理異常,而yield()不用。

sleep()方法具體具體有兩個:

//讓當前執行緒暫停millis毫秒,並進入阻塞。
public static native void sleep(long millis)
//讓當前執行緒暫停millis毫秒+nanos納秒(毫微秒),並進入阻塞
public static void sleep(long millis, int nanos)

兩個方法都會受系統計時器和執行緒排程器的精度和準確性影響。

守護執行緒

Java中有兩類執行緒:使用者執行緒(User Thread) 和 守護執行緒(Daemon Thread)。

腦海裡想到一個畫面,以前魂鬥羅之類的遊戲闖關,往往會有一個大boss,boss身邊圍繞著許許多多的“打工仔”小怪。boss只要一死,小兵統統GG,boss只要還活著,小兵就會一直戰鬥。

我們可以把boss看成使用者執行緒,把小兵看成守護執行緒。JVM例項中只要有一個非守護執行緒還在執行,守護執行緒就必須工作。當最後一個非守護執行緒結束的時候,守護執行緒就隨著JVM一起結束了。

我們熟知的垃圾回收器就是一個典型的守護執行緒,而main主執行緒是一個使用者執行緒。

主要方法

  • public final void setDaemon(boolean on):通過執行緒物件呼叫,傳入引數為true,即將該執行緒設定為守護執行緒。
  • public final boolean isDaemon():判斷執行緒物件是否為守護執行緒。

為了理解更加深刻,可以簡單測試一下:

    public static void main(String[] args) throws InterruptedException {
        //建立執行緒物件
        Thread t = new Thread(new Runnable(){
            @Override
            public void run() {
                for(int i = 0;i<100;i++){
                    System.out.println(Thread.currentThread().getName()+" : "+i);
                    
                }
            }
        });
        //設定為守護執行緒
        t.setDaemon(true);
        //啟動守護執行緒
        t.start();
        //為了更明顯,主執行緒睡眠100毫秒
        Thread.sleep(100);
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName()+" : "+i);
        }
    }

測試結果如下:

當前程式中,除了main主執行緒外沒有其他非守護執行緒的執行緒了,因此,main執行緒結束之後,所有守護執行緒也將結束。

還有一個值得去注意的點:如果所有非守護執行緒的執行緒結束,守護執行緒也將結束,守護執行緒中finally塊中的程式碼也不會執行(這個可以自行檢驗一下),因此不能依賴守護執行緒完成清理或者收尾工作,因為你完全不知道自己下一秒守護執行緒是否還健在。

需要注意

  • 由守護執行緒建立的執行緒預設也是守護執行緒,由使用者執行緒建立的也就是使用者執行緒。
  • 線上程啟動(start)之後,不允許將執行緒設定為守護執行緒(setDaemon),否則將會丟擲java.lang.IllegalThreadStateException異常。
    參考:Java中守護執行緒的總結

優先順序

我們說過,各個處於就緒狀態執行緒等待資源排程是按照一定規則的,這個規則就是執行緒擁有的優先順序。

以下參考《Java程式設計思想》:

JDK有10個優先順序,但它和大多數作業系統都不能很好地進行對映。例如Windows有7個優先順序且不是固定的,所以這種對映關係也是不確定的。所以在調整優先順序的時候,使用下面三種常量,具有更好的移植性。

    //執行緒可有的最小優先順序
    public final static int MIN_PRIORITY = 1;

   //預設優先順序
    public final static int NORM_PRIORITY = 5;

    //執行緒可有的最大優先順序
    public final static int MAX_PRIORITY = 10;

排程器會傾向於讓優先順序較高的執行緒先執行,但並不意味著優先順序較低的執行緒將得不到執行。

進行試驗:

  • 定義一個實現Runnable介面的類。
class PDemo implements Runnable {
    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        for (int i = 0; i < 10; i++) {
            System.out.println(name + ":" + i);
        }
    }
}
  • 建立兩個執行緒物件。
Thread t1 = new Thread(new PDemo(),"A");
Thread t2 = new Thread(new PDemo(),"B");
  • 可以通過執行緒物件的getPriority()方法獲取當前優先順序。
//預設情況下,執行緒的優先順序為5
System.out.println(t1.getPriority());//5
System.out.println(t2.getPriority());//5
  • 通過執行緒物件的setPriority(int newPriority)方法設定優先順序。
//設定執行緒優先順序
t1.setPriority(Thread.MIN_PRIORITY);
t2.setPriority(Thread.MAX_PRIORITY);
  • 呼叫執行緒物件的start()方法啟動執行緒。
t1.start();
t2.start();
  • 每個執行緒預設的優先順序都與建立它的父執行緒的優先順序的相同。
  • main執行緒的優先順序是NORM_PRIORITY為5。

棄用三兄弟

stop()

resume

suspend

中斷三兄弟

interrupt()

public void interrupt()是Thread類的一個例項方法,說是說用來中斷執行緒,但其實只是給執行緒設定了一個"中斷"標誌(true) ,執行緒仍然會繼續執行,使用者可以監視執行緒的狀態並做出相應處理。

官方文件是這麼說的:
執行緒呼叫interrupt()將會把標誌位設定為true,除此之外,情況不同,處理不同:

  • 如果這個執行緒由於wait(),join(),sleep()等方法陷入等待狀態時,它的中斷狀態將被清除(也就是true重新變為false),而且會收到一個InterruptedException

但是我按照下面程式碼測試了一下,join()和sleep()都能成功檢驗,但是wait()檢驗不出,不知問題出在哪,評論區大神求助!!

下面這倆目前還沒有接觸到,以後有機會做總結:

  • 如果這個執行緒由於java.nio.channels.InterruptibleChannel中的IO操作發生阻塞,執行緒還將收到一個ClosedByInterruptException
  • 如果這個執行緒在Selector中被阻塞,它可能帶有一個非零值,從選擇操作立即返回,就像呼叫了選擇器的wakeup()方法一樣。

interrupted()

public static boolean interrupted()是靜態方法,內部呼叫當前執行緒的isInterrupted方法,會重置當前執行緒的中斷狀態。也就是說,如果執行緒被設定為中斷標誌,第一次呼叫此方法將會返回true,並將中斷標誌重置,第二次呼叫該方法,將會返回false。

isInterrupted()

public boolean isInterrupted()是例項方法,測試當前執行緒的物件是否被中斷,而不會重置當前執行緒的中斷狀態。


關於這三個方法的測試,可以參考這篇部落格,非常詳細:
https://blog.csdn.net/zhuyong7/article/details/80852884

安全終止執行緒

以下內容參考:《Java併發程式設計的藝術》

上面提到,interrupt()方法只是給執行緒標誌為“中斷”狀態,並不會讓執行緒真正中斷,我們可以對標識位進行監測並做出相應處理,比如,我們可以通過中斷操作與自定義變數來控制是否需要停止任務並終止該執行緒。

定義一個執行緒內部類。

    private static class Runner implements Runnable {
        private long i;
        //定義變數作為標識位,用volatile修飾,自身擁有可見性和原子性
        private volatile boolean on = true;

        @Override
        public void run() {
            //對自定義標識位以及中斷標識進行校驗
            while (on && !Thread.currentThread().isInterrupted()) {
                i++;
            }
            System.out.println("Count i = " + i);
        }
        //取消操作
        public void cancel() {
            on = false;
        }
    }

利用標識位優雅地中斷或結束執行緒。

    public static void main(String[] args) throws InterruptedException {
        Runner one = new Runner();
        Thread countThread = new Thread(one, "CountThread");
        countThread.start();
        //睡眠一秒,main執行緒對CountThread進行中斷,使CountThread能夠感知中斷而結束
        TimeUnit.SECONDS.sleep(1);
        countThread.interrupt();
        Runner two = new Runner();
        countThread = new Thread(two, "CountThread");
        countThread.start();
        //睡眠一秒,main執行緒對Runner two進行取消,使CountThread能夠感知on為false而結束
        TimeUnit.SECONDS.sleep(1);
        two.cancel();
    }

參考連結:《Java併發程式設計的藝術》、《瘋狂Java講