【Java】Java線程中斷(Interrupt)與阻塞(park)的區別

對於很多剛接觸編程的人來說,對於線程中斷和線程阻塞兩個概念,經常性是混淆起來用,單純地認爲線程中斷與線程阻塞的概念是一致的,都是值線程運行狀態的停止。其實這個觀點是錯誤的,兩者之前有很大的區別,下文就着重介紹兩者之間的區別。

線程中斷

在一個線程正常結束之前,如果被強制終止,那麼就有可能造成一些比較嚴重的後果,設想一下如果現在有一個線程持有同步鎖,然後在沒有釋放鎖資源的情況下被強制休眠,那麼這就造成了其他線程無法訪問同步代碼塊。因此我們可以看到在 Java 中類似 Thread#stop() 方法被標爲 @Deprecated

針對上述情況,我們不能直接將線程給終止掉,但有時又必須將讓線程停止運行某些代碼,那麼此時我們必須有一種機制讓線程知道它該停止了。Java 爲我們提供了一個比較優雅的做法,即可以通過 Thread#interrupt() 給線程該線程一個標誌位,讓該線程自己決定該怎麼辦。

接下來就用代碼來延時下 interrupt() 的作用:

public class InterruptDemo {

    static class MyThread implements Runnable {
        @Override
        public void run() {
            for (int i= 0; !Thread.currentThread().isInterrupted() && i < 200000; i++) {
                System.out.println(Thread.currentThread().getName() + ":i = " + i);
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread myThread = new Thread(new MyThread());
        myThread.start();

        // 讓線程運行一段時間
        Thread.sleep(5);

        myThread.interrupt();

        // 等待 myThread 運行停止
        myThread.join();
        System.out.println("end");
    }

}

以上代碼的運行結果如下:

可以看到,當前線程並沒有按 for 循環中的結束量 20000 去跑,而是在被中斷後,停止了當前了 for 循環。所以我們可以利用 interrupt 配置線程使用,使得線程在一定的位置停止下來。

不過到這裏可能會讓人產生一些疑惑,因爲在這裏看起來,當前線程像是被阻塞掉了,其實並不是的,我們可以利用下面這段代碼來演示下:

public class InterruptDemo {

    static class MyThread implements Runnable {
        @Override
        public void run() {
            for (int i= 0; i < 200000; i++) {
                System.out.println(Thread.currentThread().getName() + ":i = " + i);
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread myThread = new Thread(new MyThread());
        myThread.start();

        // 讓線程運行一段時間
        Thread.sleep(5);

        myThread.interrupt();

        // 等待 myThread 運行停止
        myThread.join();
        System.out.println("end");
    }

}

上面這段代碼的運行結果如下:

可見,線程一直打印到 20000,執行完畢後推出線程,並沒有像我們預料中在某處中斷。所以我們可以得出結論:單純用 interrupt() 中斷線程方法並不能停止當前正在運行的線程,需要配合其他方法才能正確停止線程。

瞭解完中斷的基本概念後,線程的中斷還有需要其他需要注意的點:

  • 設置線程中斷後,線程內調用 wait()join()slepp() 方法中的一種,都會拋出 InterruptedException 異常,且中斷標誌位被清除,重新設置爲 false;
  • 當線程被阻塞,比如調用了上述三個方法之一,那麼此時調用它的 interrupt() 方法,也會產生一個 InterruptedException 異常。因爲沒有佔有 CPU 的線程是無法給自己設置中斷狀態位置的;
  • 嘗試獲取一個內部鎖的操作(進入一個 synchronized 塊)是不能被中斷的,但是 ReentrantLock 支持可中斷的獲取模式:tryLock(long time, TimeUnit unit)
  • 當代碼調用中需要拋出一個 InterruptedException,捕獲之後,要麼繼續往上拋,要麼重置中斷狀態,這是最安全的做法。

線程阻塞

上面講完了線程中斷,它其實只是一個標誌位,並不能讓線程真正的停止下來,那麼接下來就來介紹如何真正讓線程停止下來。

對於這個問題,Java 中提供了一個較爲底層的併發工具類:LockSupport,該類中的核心方法有兩個:park(Object blocker) 以及 unpark(Thread thred),前者表示阻塞指定線程,後者表示喚醒指定的線程。

// java.util.concurrent.locks.LockSupport

public static void park(Object blocker) {
    Thread t = Thread.currentThread();
    setBlocker(t, blocker);
    UNSAFE.park(false, 0L);
    setBlocker(t, null);
}

public static void unpark(Thread thread) {
    if (thread != null)
        UNSAFE.unpark(thread);
}

該方法在 Java 的語言層面上比較簡單,最終也是去調用 UNSAFE 中的 native 方法。真正涉及到底層的東西需要去理解 JVM 的源碼,這裏就不做太多的介紹。不過我們可以用一個簡單的例子來演示下這兩個方法:

public class LockSupportDemo {

    static class MyThread implements Runnable {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "開始執行");
            LockSupport.park();
            System.out.println(Thread.currentThread().getName() + "執行結束");
        }
    }

    public static void main(String[] args) throws InterruptedException {

        Thread thread = new Thread(new MyThread(), "線程:MyThread");
        thread.start();
        Thread.sleep(100);
        System.out.println(Thread.currentThread().getName() + "主線程執行中");
        LockSupport.unpark(thread);
        System.out.println(Thread.currentThread().getName() + "主線程執行結束");
    }

}

上述代碼的執行結果爲:

線程:MyThread開始執行
main主線程執行中
線程:MyThread執行結束
main主線程執行結束

可以看到,myThread 線程在開始執行後停止了下來,等到主線程重新調用 LockSupport.unpark(thread) 後才重新開始執行。

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