java線程暫停和恢復,線程暫停和恢復

本節講解的內容是線程的初始化、線程啟動、線程中斷、suspend()、resume()、stop()和優雅的關閉線程。

1.線程初始化和線程的啟動

首先看下Thread的構造方法:

    public Thread(Runnable target, String name) {
        init(null, target, name, 0);
    }

當然Thread有多個構造方法,都是調用init方法初始化一個線程。看下init的定義:

 private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
                      ...
                      }

包含了所屬線程組、Runable對象、線程名稱、線程的棧大小、訪問控制權和inheritThreadLocals。當我們通過new關鍵字創建線程的時候,默認由當前線程來進行空間分配的,而創建出來的線程繼承了當前線程的是否為守護線程、線程優先級、contextClassLoader以及ThreadLocal。創建好的線程對象初始化完成後,在堆內存中等待執行。

注意,創建線程時最好執行線程的名字,方便通過命令jstack分析堆棧信息和問題排查。如果不手動指定線程名稱,默認是Thread-” + nextThreadNum()。

線程啟動是調用start()方法,當前線程同步告訴Java虛擬機,如果線程規劃器空閑,應該立即啟動線程。start方法不可以重複調用。

面試中經常會問到直接運行run()方法會怎麼?

首先run方法只是個普通方法,直接運行不會啟動一個線程,且當前只有一個主線程(當前線程),程序順序執行,達不到多線程的目的。且run方法可以重複執行。

2.線程的中斷

當我們執行一個阻塞IO或者長時間沒有響應的任務時,我們需要結束線程以避免長時間等待。Thread.interrupt(),不是立馬中斷線程,而是設置一個中斷標識位。Java中的阻塞函數比如Thread.sleep,Object.wait,Thread.join等,會不斷地輪訓檢測線程中斷標識位是否為true,如果為true,會立馬拋出InterruptedException,同時把標識位設置為false。在下面的程序中,有兩個線程,worker和sleeper,線程中斷後,觀察下輸出的標識位:

public class TestInterrupt {
    public static void main(String[] args) {
        Thread worker = new Thread(()->{
            while (true){}
        }, "work-thread");

        Thread sleeper = new Thread(()->{
            while (true){
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "sleeper-thread");

        worker.start();
        sleeper.start();
        worker.interrupt();
        sleeper.interrupt();
        System.out.println("work-thread interrupt is " + worker.isInterrupted());
        System.out.println("sleeper-thread interrupt is " + sleeper.isInterrupted());
    }
}
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at net.zhifou.concurrent.base.TestInterrupt.lambda$main$1(TestInterrupt.java:16)
	at java.lang.Thread.run(Thread.java:748)
work-thread interrupt is true
sleeper-thread interrupt is false

當中斷阻塞狀態的sleeper線程,立馬拋出了InterruptedException異常,重置中斷標識位後輸出中斷標識位為false,worker線程中斷標識位仍為true。

Thread的兩個方法,interrupted和isInterrupted,都是返回中斷標識位。我們先看下方法定義:

    public static boolean interrupted() {
        return currentThread().isInterrupted(true);
    }
    
       public boolean isInterrupted() {
        return isInterrupted(false);
    }

然後再看下isInterrupted方法的實現:

    /**
     * Tests if some Thread has been interrupted.  The interrupted state
     * is reset or not based on the value of ClearInterrupted that is
     * passed.
     */
    private native boolean isInterrupted(boolean ClearInterrupted);

這個是native方法,看不到實現沒關係,看下注釋,根據ClearInterrupted的值重置或不重置。isInterrupted傳入的是false,不清除中斷的標識位。而interrupted,傳入的是ture,則返回中斷標識位後,然後清除中斷標識位。

3. suspend()、resume()、stop()

suspend()、resume()、stop()這三個方法,分別對應着,線程暫停、線程恢復、線程停止。觀察Java源碼,都被標識為@Deprecated,不建議使用了。

首先我們看suspend()、resume(),它們必須成對出現,且有先後順序。那麼為什麼會被廢棄呢?

因為線程調用suspend程序暫停後,不會釋放任何資源,包括鎖,且一直處於睡眠狀態,容易引發死鎖。而resume因為要和suspend成對出現,所以也沒有存在的必要。

那麼stop方法為什麼不建議使用呢?我們先看源碼:

    @Deprecated
    public final void stop() {
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            checkAccess();
            if (this != Thread.currentThread()) {
                security.checkPermission(SecurityConstants.STOP_THREAD_PERMISSION);
            }
        }
        // A zero status value corresponds to "NEW", it can't change to
        // not-NEW because we hold the lock.
        if (threadStatus != 0) {
            resume(); // Wake up thread if it was suspended; no-op otherwise
        }

        // The VM can handle all thread states
        stop0(new ThreadDeath());
    }

最終通過調用stop0(new ThreadDeath())函數,stop0是Native函數,參數是ThreadDeath,ThreadDeath是一個異常對象,該對象從Native層拋到了Java層。線程運行中遇到異常,會導致線程停止,不過不會引起程序退出。

線程是因為異常而停止的,鎖會釋放,但是不會釋放其他的資源,比如打開的socket連接或者IO,多不會關閉,引發內存泄漏。

很多時候為了保證數據安全,線程中會編寫同步代碼,如果當線程正在執行同步代碼時,此時調用stop,引起拋出異常,導致線程持有的鎖會全部釋放,此時就不能確保數據的安全性,出現無法預期的錯亂數據,還有可能導致存在需要被釋放的資源得不到釋放,引發內存泄露。所以用stop停止線程是不推薦的。

4.優雅的終止線程

關閉線程,肯定等到線程釋放掉資源,以免造成內存泄漏,所以不建議使用stop方法。那麼應該怎麼關閉線程呢?答案是兩階段終止模式。

兩階段終止模式,顧名思義,分兩個階段。第一個階段,向線程發送終止指令,記住這裡只是設置一個終止標識位。第二階段,線程響應指令,然後終止。這裡就用到了,Thread.interrupt()方法來設置終止標識位。我們先看一張圖如下:

面試官:線程如何啟動和優雅的終止

想終止線程,也就是線程是終止狀態,如上圖所示,線程只能是RUNNABLE狀態才可以到終止狀態。所以線程目前狀態可以能是RUNNABLE狀態,或者是休眠狀態。當線程是休眠狀態,當我們設置中斷標識位時,線程立馬會拋出InterruptedException,並且JVM會清除中斷標識位,所以在捕獲InterruptedException異常後,需要重新設置中斷標識位。如下代碼:

public class StopThread {

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            System.out.println("worker-Thread start ...");
            while(!Thread.currentThread().isInterrupted()) {
                System.out.println("worker-Thread working ...");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    Thread.currentThread().interrupt();
                }
            }
            System.out.println("worker-Thread done ...");
        }, "worker-Thread");

        t.start();
        Thread.sleep(10000);
        t.interrupt();
    }
}

線程t每次執行while內代碼,休眠1000ms,然後繼續執行,主線程在休眠10000ms後,線程t設置中斷標識位,此時,如果線程t還在sleep中,則拋出InterruptedException異常,JVM清除中斷標識位,重新設置中斷標識位後,下次執行while代碼,條件不符合,線程執行完畢。這樣,就完成了兩階段模式。

總結:

  1. 線程啟動使用start方法,而線程run方法,不會啟動一個線程。
  2. Thread.interrupt(),設置中斷標識位,具體是否結束線程,線程自己控制。
  3. suspend()、resume()、stop(),這三個方法已經不建議使用,有可能造成死鎖和內存泄漏。
  4. 通過兩階段提交模式,來優雅的終止線程。

原創文章,作者:投稿專員,如若轉載,請註明出處:https://www.506064.com/zh-hk/n/228015.html

(0)
打賞 微信掃一掃 微信掃一掃 支付寶掃一掃 支付寶掃一掃
投稿專員的頭像投稿專員
上一篇 2024-12-09 21:30
下一篇 2024-12-09 21:30

相關推薦

發表回復

登錄後才能評論