本系列文章經補充和完善,已修訂整理成書《Java編程的邏輯》(馬俊昌著),由機械工業出版社華章分社出版,於2018年1月上市熱銷,讀者好評如潮!各大網店和書店有售,歡迎購買:京東自營連接 html
![]()
本節主要討論一個問題,如何在Java中取消或關閉一個線程?java
咱們知道,經過線程的start方法啓動一個線程後,線程開始執行run方法,run方法運行結束後線程退出,那爲何還須要結束一個線程呢?有多種狀況,好比說:git
Java的Thread類定義了以下方法:github
public final void stop() 複製代碼
這個方法看上去就能夠中止線程,但這個方法被標記爲了過期,簡單的說,咱們不該該使用它,能夠忽略它。編程
在Java中,中止一個線程的主要機制是中斷,中斷並非強迫終止一個線程,它是一種協做機制,是給線程傳遞一個取消信號,可是由線程來決定如何以及什麼時候退出,本節咱們主要就是來理解Java的中斷機制。swift
Thread類定義了以下關於中斷的方法:bash
public boolean isInterrupted() public void interrupt() public static boolean interrupted() 複製代碼
這三個方法名字相似,比較容易混淆,咱們解釋一下。isInterrupted()和interrupt()是實例方法,調用它們須要經過線程對象,interrupted()是靜態方法,實際會調用Thread.currentThread()操做當前線程。服務器
每一個線程都有一個標誌位,表示該線程是否被中斷了。微信
interrupt()對線程的影響與線程的狀態和在進行的IO操做有關,咱們先主要考慮線程的狀態:網絡
若是線程在運行中,且沒有執行IO操做,interrupt()只是會設置線程的中斷標誌位,沒有任何其它做用。線程應該在運行過程當中合適的位置檢查中斷標誌位,好比說,若是主體代碼是一個循環,能夠在循環開始處進行檢查,以下所示:
public class InterruptRunnableDemo extends Thread {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
// ... 單次循環代碼
}
System.out.println("done ");
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new InterruptRunnableDemo();
thread.start();
Thread.sleep(1000);
thread.interrupt();
}
}
複製代碼
線程執行以下方法會進入WAITING狀態:
public final void join() throws InterruptedException public final void wait() throws InterruptedException 複製代碼
執行以下方法會進入TIMED_WAITING狀態:
public final native void wait(long timeout) throws InterruptedException;
public static native void sleep(long millis) throws InterruptedException;
public final synchronized void join(long millis) throws InterruptedException 複製代碼
在這些狀態時,對線程對象調用interrupt()會使得該線程拋出InterruptedException,須要注意的是,拋出異常後,中斷標誌位會被清空,而不是被設置。好比說,執行以下代碼:
Thread t = new Thread (){
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println(isInterrupted());
}
}
};
t.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
t.interrupt();
複製代碼
程序的輸出爲false。
InterruptedException是一個受檢異常,線程必須進行處理。咱們在異常處理中介紹過,處理異常的基本思路是,若是你知道怎麼處理,就進行處理,若是不知道,就應該向上傳遞,一般狀況下,你不該該作的是,捕獲異常而後忽略。
捕獲到InterruptedException,一般表示但願結束該線程,線程大概有兩種處理方式:
第一種方式的示例代碼以下:
public void interruptibleMethod() throws InterruptedException{
// ... 包含wait, join 或 sleep 方法
Thread.sleep(1000);
}
複製代碼
第二種方式的示例代碼以下:
public class InterruptWaitingDemo extends Thread {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
try {
// 模擬任務代碼
Thread.sleep(2000);
} catch (InterruptedException e) {
// ... 清理操做
// 重設中斷標誌位
Thread.currentThread().interrupt();
}
}
System.out.println(isInterrupted());
}
public static void main(String[] args) {
InterruptWaitingDemo thread = new InterruptWaitingDemo();
thread.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
thread.interrupt();
}
}
複製代碼
若是線程在等待鎖,對線程對象調用interrupt()只是會設置線程的中斷標誌位,線程依然會處於BLOCKED狀態,也就是說,interrupt()並不能使一個在等待鎖的線程真正"中斷"。咱們看段代碼:
public class InterruptSynchronizedDemo {
private static Object lock = new Object();
private static class A extends Thread {
@Override
public void run() {
synchronized (lock) {
while (!Thread.currentThread().isInterrupted()) {
}
}
System.out.println("exit");
}
}
public static void test() throws InterruptedException {
synchronized (lock) {
A a = new A();
a.start();
Thread.sleep(1000);
a.interrupt();
a.join();
}
}
public static void main(String[] args) throws InterruptedException {
test();
}
}
複製代碼
test方法在持有鎖lock的狀況下啓動線程a,而線程a也去嘗試得到鎖lock,因此會進入鎖等待隊列,隨後test調用線程a的interrupt方法並等待線程線程a結束,線程a會結束嗎?不會,interrupt方法只會設置線程的中斷標誌,而並不會使它從鎖等待隊列中出來。
咱們稍微修改下代碼,去掉test方法中的最後一行a.join,即變爲:
public static void test() throws InterruptedException {
synchronized (lock) {
A a = new A();
a.start();
Thread.sleep(1000);
a.interrupt();
}
}
複製代碼
這時,程序就會退出。爲何呢?由於主線程再也不等待線程a結束,釋放鎖lock後,線程a會得到鎖,而後檢測到發生了中斷,因此會退出。
在使用synchronized關鍵字獲取鎖的過程當中不響應中斷請求,這是synchronized的侷限性。若是這對程序是一個問題,應該使用顯式鎖,後面章節咱們會介紹顯式鎖Lock接口,它支持以響應中斷的方式獲取鎖。
若是線程還沒有啓動(NEW),或者已經結束(TERMINATED),則調用interrupt()對它沒有任何效果,中斷標誌位也不會被設置。好比說,如下代碼的輸出都是false。
public class InterruptNotAliveDemo {
private static class A extends Thread {
@Override
public void run() {
}
}
public static void test() throws InterruptedException {
A a = new A();
a.interrupt();
System.out.println(a.isInterrupted());
a.start();
Thread.sleep(100);
a.interrupt();
System.out.println(a.isInterrupted());
}
public static void main(String[] args) throws InterruptedException {
test();
}
}
複製代碼
若是線程在等待IO操做,尤爲是網絡IO,則會有一些特殊的處理,咱們沒有介紹過網絡,這裏只是簡單介紹下。
咱們重點介紹另外一種狀況,InputStream的read調用,該操做是不可中斷的,若是流中沒有數據,read會阻塞 (但線程狀態依然是RUNNABLE),且不響應interrupt(),與synchronized相似,調用interrupt()只會設置線程的中斷標誌,而不會真正"中斷"它,咱們看段代碼。
public class InterruptReadDemo {
private static class A extends Thread {
@Override
public void run() {
while(!Thread.currentThread().isInterrupted()){
try {
System.out.println(System.in.read());
} catch (IOException e) {
e.printStackTrace();
}
}
System.out.println("exit");
}
}
public static void main(String[] args) throws InterruptedException {
A t = new A();
t.start();
Thread.sleep(100);
t.interrupt();
}
}
複製代碼
線程t啓動後調用System.in.read()從標準輸入讀入一個字符,不要輸入任何字符,咱們會看到,調用interrupt()不會中斷read(),線程會一直運行。
不過,有一個辦法能夠中斷read()調用,那就是調用流的close方法,咱們將代碼改成:
public class InterruptReadDemo {
private static class A extends Thread {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
try {
System.out.println(System.in.read());
} catch (IOException e) {
e.printStackTrace();
}
}
System.out.println("exit");
}
public void cancel() {
try {
System.in.close();
} catch (IOException e) {
}
interrupt();
}
}
public static void main(String[] args) throws InterruptedException {
A t = new A();
t.start();
Thread.sleep(100);
t.cancel();
}
}
複製代碼
咱們給線程定義了一個cancel方法,在該方法中,調用了流的close方法,同時調用了interrupt方法,此次,程序會輸出:
-1
exit
複製代碼
也就是說,調用close方法後,read方法會返回,返回值爲-1,表示流結束。
以上,咱們能夠看出,interrupt方法不必定會真正"中斷"線程,它只是一種協做機制,若是不明白線程在作什麼,不該該貿然的調用線程的interrupt方法,覺得這樣就能取消線程。
對於以線程提供服務的程序模塊而言,它應該封裝取消/關閉操做,提供單獨的取消/關閉方法給調用者,相似於InterruptReadDemo中演示的cancel方法,外部調用者應該調用這些方法而不是直接調用interrupt。
Java併發庫的一些代碼就提供了單獨的取消/關閉方法,好比說,Future接口提供了以下方法以取消任務:
boolean cancel(boolean mayInterruptIfRunning);
複製代碼
再好比,ExecutorService提供了以下兩個關閉方法:
void shutdown();
List<Runnable> shutdownNow();
複製代碼
Future和ExecutorService的API文檔對這些方法都進行了詳細說明,這是咱們應該學習的方式。關於這兩個接口,咱們後續章節介紹。
本節主要介紹了在Java中如何取消/關閉線程,主要依賴的技術是中斷,但它是一種協做機制,不會強迫終止線程,咱們介紹了線程在不一樣狀態和IO操做時對中斷的反應,做爲線程的實現者,應該提供明確的取消/關閉方法,並用文檔描述清楚其行爲,做爲線程的調用者,應該使用其取消/關閉方法,而不是貿然調用interrupt。
從65節到本節,咱們介紹的都是關於線程的基本內容,在Java中還有一套併發工具包,位於包java.util.concurrent下,裏面包括不少易用且高性能的併發開發工具,從下一節開始,咱們就來討論它,先從最基本的原子變量和CAS操做開始。
(與其餘章節同樣,本節全部代碼位於 github.com/swiftma/pro…)
未完待續,查看最新文章,敬請關注微信公衆號「老馬說編程」(掃描下方二維碼),深刻淺出,老馬和你一塊兒探索Java編程及計算機技術的本質。用心原創,保留全部版權。