對於很多剛接觸編程的人來說,對於線程中斷和線程阻塞兩個概念,經常性是混淆起來用,單純地認爲線程中斷與線程阻塞的概念是一致的,都是值線程運行狀態的停止。其實這個觀點是錯誤的,兩者之前有很大的區別,下文就着重介紹兩者之間的區別。
線程中斷
在一個線程正常結束之前,如果被強制終止,那麼就有可能造成一些比較嚴重的後果,設想一下如果現在有一個線程持有同步鎖,然後在沒有釋放鎖資源的情況下被強制休眠,那麼這就造成了其他線程無法訪問同步代碼塊。因此我們可以看到在 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)
後才重新開始執行。