摘要
在java中,想要讓一個線程停下來,有三種辦法:
(1)採用退出標誌,使得run方法執行完之後線程自然終止。
(2)使用stop強行終止線程,但該方法由於安全問題已經被deprecated。
(3)使用中斷機制。
引入
第一種方法沒特別之處,無非是在覆蓋Runnable接口之時對run方法中添加狀態標識邏輯。比如:
public class MyThread extends Thread
{
private boolean running;
@Override
public run(){
if(running){
//....business methods
}
}
}
下面詳細討論中斷機制。
這裏的中斷是軟中斷,即在某個線程中可以將目標線程的中斷標誌位置位,然後在目標線程進行中斷檢查的時候來判斷自己應當如何響應中斷。
特別的,可以以此來停止目標線程。中斷的侷限性,無法對出於死鎖狀態的線程起作用。
簡單原理:
java.lang.Thread
類中提供了一個方法來將線程的中斷標誌位置位:
Thread.interrupt()
方法,其函數申明爲:public void interrupt();
該方法會將線程的中斷標誌位置位。
java.lang.Thread
類還兩個方法來檢查中斷標誌:
public static boolean interrupted()
{
return currentThread().isInterrupted(true);
}
public boolean isInterrupted()
{
return isInterrupted(false);
}
這兩個方法分別調用同一個原生方法來檢查中斷狀態並有所不同。
一方面interrupted()
是靜態方法,可以使用Thread.interrupted()
的方法來調用。
而isInterrupted()
則需要一個實例對象作爲隱式參數,使用threadInstance.isInterrupted()
來調用。
另一方面,interrupted在調用後會立即將線程中斷標誌位復位,而isInterrupted則不會這麼做。
非阻塞線程的中斷問題:
調用Thread.interrupt將目標線程中斷標誌位置位,然後在目標線程中檢查中斷標誌位。
如果檢查到中斷之後使用throw new InterruptedException()拋出一個異常,然後使用catch語句來捕獲這個異常,則可以在這裏完成一些中斷邏輯。
特別的,可以使用這種方法退出線程。下面是這種退出方式的舉例。
package com.kingbaron.jinterrupt;
/**
* Created by kingbaron on 2016/3/17.
*/
public class UnblockingInterrupt extends Thread{
@Override
public void run(){
super.run();
try{
for(int i=0;i<10000;i++)
{
if(Thread.interrupted())
//if(this.isInterrupted())
{
System.out.println("I'm interrupted and I'm going out");
throw new InterruptedException();
}
System.out.println("i="+(i+1));
}
System.out.println("I'm below the for clauses and I have no chance to run here");
}catch(InterruptedException e){
System.out.println("I'm in the catch clauses of the method UnblockingInterrupt.run()");
e.printStackTrace();
}
}
}
package com.kingbaron.jinterrupt;
/**
* Created by kingbaron on 2016/3/17.
*/
public class TestUnblockingInterrupt {
public static void main(String[] args){
try{
UnblockingInterrupt thread=new UnblockingInterrupt();
thread.start();
Thread.sleep(2000);
thread.interrupt();
}catch(InterruptedException e)
{
System.out.println("main catch");
e.printStackTrace();
}
}
}
輸出結果(關鍵部分)爲:
i=277919
i=277920
I'm interrupted and I'm going out
I'm in the catch clauses of the method UnblockingInterrupt.run
java.lang.InterruptedException
at com.kingbaron.jinterrupt.UnblockingInterrupt.run(UnblockingInterrupt.java:16)
可以看到,當線程thread的中斷標誌位被main線程中使用thread.interrupt置位後,在執行thread線程的for循環的if條件檢查時,
Thread.interrupted()返回true隨後在其中拋出一個InterruptedException異常,再使用catch子句捕獲該異常,由於run方法中在catch子句之後再無語句,
故線程從run方法返回,thread線程就此終止。
阻塞線程的中斷問題。
首先,一個線程出於阻塞中斷狀態(實際上還包括等待狀態)的原因,常見的有線程調用了thread.sleep、
thread.join、thread.wait、1.5中的condition.await、以及可中斷的通道上的 I/O 操作方法等。
如果一個線程處於阻塞狀態,若將其中斷標誌位置爲,則會在產生阻塞的語句出拋出一個InterruptedException異常,
並且在拋出異常後立即將線程的中斷標示位復位,即重新設置爲false。
拋出異常是爲了線程從阻塞狀態醒過來,並在結束線程前讓程序員有足夠的時間來處理中斷請求。
換言之,中斷將一個處於阻塞狀態的線程的阻塞狀態解除。
注意,synchronized在獲鎖的過程中是不能被中斷的,意思是說如果產生了死鎖,則不可能被中斷(請參考後面的測試例子)。
與synchronized功能相似的reentrantLock.lock()方法也是一樣,它也不可中斷的。
即如果發生死鎖,那麼reentrantLock.lock()方法無法終止,如果調用時被阻塞,則它一直阻塞到它獲取到鎖爲止。
但是如果調用帶超時的tryLock方法reentrantLock.tryLock(long timeout, TimeUnit unit),那麼如果線程在等待時被中斷,將拋出一個InterruptedException異常,這是一個非常有用的特性,因爲它允許程序打破死鎖。
你也可以調用reentrantLock.lockInterruptibly()方法,它就相當於一個超時設爲無限的tryLock方法。
沒有任何語言方面的需求一個被中斷的線程應該終止。中斷一個線程只是爲了引起該線程的注意,被中斷線程可以決定如何應對中斷。
某些線程非常重要,以至於它們應該不理會中斷,而是在處理完拋出的異常之後繼續執行,但是更普遍的情況是,一個線程將把中斷看作一個終止請求。
這種線程的run方法遵循如下形式:
public void run() {
try {
...
/*
* 不管循環裏是否調用過線程阻塞的方法如sleep、join、wait,這裏還是需要加上
* !Thread.currentThread().isInterrupted()條件,雖然拋出異常後退出了循環,顯
* 得用阻塞的情況下是多餘的,但如果調用了阻塞方法但沒有阻塞時,這樣會更安全、更及時。
*/
while (!Thread.currentThread().isInterrupted()&& more work to do) {
do more work
}
} catch (InterruptedException e) {
//線程在wait或sleep期間被中斷了
} finally {
//線程結束前做一些清理工作
}
}
在這裏,應當注意一個原則:儘量不要在底層代碼中捕獲InterruptedException原則。因爲該異常一旦被捕獲,線程的中斷標誌位就會被置位。
對於底層代碼而言,如果代碼會拋出不知道應當如何應對的InterruptException異常,可以有下面兩個選擇。
(1)捕獲到InterruptedException異常後將中斷標誌位置位,讓外層代碼根據檢查中斷標誌位來判斷是否中斷。
public subTask(){
try{
//...the works may throw a InterruptedException
}catch(InterruptedException e){
Thread.currentThread.interrupt();
}
}
(2)更推薦的方法是,底層代碼不捕獲InterruptedException,直接將其拋給外層代碼去解決
public subTask() throws InterruptedException
{
//...
}
中斷失效的情況之臨界區:
進入臨界區的代碼是不允許中斷的。這一點很好理解,臨界區是並行問題爲了保護臨界資源的互斥訪問而特地加鎖的,
一旦可以中斷,那麼鎖的存在也就沒有任何意義了。特別的,形成了死鎖的線程會分別處於阻塞狀態,但是它們都無法被中斷。
常見的有進入synchronized塊的代碼以及Lock.lock()之後沒能得到鎖而處於阻塞的狀態。通常可以使用Lock.lockInterruptibly()來代替Lock.lock(),
因爲Lock.lockInterruptibly()可以接受中斷,這個中斷指的是,既然沒能得到鎖進入臨界區,與其阻塞不如做些別的什麼事。
一旦進入臨界區,任何方法都不得中斷。
中斷失效的情況之不可中斷I/O:
自java 1.4之後對大量數據的I/O常用通道(channels)機制,它是可以被中斷的I/O,也就是說如果這種I/O處於阻塞狀態,可以使用中斷來解除阻塞。
但是存在着一些不可中斷的操作,比如ServerSocket.accept(),inputSteam.read()等調用interrupt()對於這幾個問題無效,因爲它們都不拋出中斷異常。如果拿不到資源,它們會無限期阻塞下去。
對於inputStream等資源,有些(實現了interruptibleChannel接口)可以通過close()方法將資源關閉,對應的阻塞也會被放開。
對於處理大型I/O時,推薦使用Channels。