深入分析Java線程中斷機(jī)制
Thread.interrupt真的能中斷線程嗎
在平時(shí)的開發(fā)過程中,相信都會(huì)使用到多線程,在使用多線程時(shí),大家也會(huì)遇到各種各樣的問題,今天我們就來說說一個(gè)多線程的問題——線程中斷。在 java中啟動(dòng)線程非常容易,大多數(shù)情況下我是讓一個(gè)線程執(zhí)行完自己的任務(wù)然后自己停掉,但是有時(shí)候我們需要取消某個(gè)操作,比如你在網(wǎng)絡(luò)下載時(shí),有時(shí)候需 要取消下載。實(shí)現(xiàn)線程的安全中斷并不是一件容易的事情,因?yàn)镴ava并不支持安全快速中斷線程的機(jī)制,這里估計(jì)很多同學(xué)就會(huì)說了,java不是提供了Thread.interrupt
方法中斷線程嗎,好吧,我們今天就從這個(gè)方法開始說起。
但是調(diào)用此方法線程真的會(huì)停止嗎?我們寫個(gè)demo看看就知道了。
- public class Main {
- private static final String TAG = "Main";
- public static void main(String[] args) {
- Thread t=new Thread(new NRunnable());
- t.start();
- System.out.println("is start.......");
- try {
- Thread.sleep(3000);
- } catch (InterruptedException e) {
- }
- t.interrupt();
- System.out.println("is interrupt.......");
- }
- public static class NRunnable implements Runnable
- {
- @Override
- public void run() {
- while(true)
- {
- System.out.println("我沒有種中斷");
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- }
- }
- }
- }
- }
如果interrutp方法能夠中斷線程,那么在打印了is interrupt…….之后應(yīng)該是沒有l(wèi)og了,我們看看執(zhí)行結(jié)果吧
is start.......
我沒有種中斷
我沒有種中斷
我沒有種中斷
我沒有種中斷
我沒有種中斷
is interrupt.......
我沒有種中斷
我沒有種中斷
我沒有種中斷
我沒有種中斷
我沒有種中斷
....
通過結(jié)果可以發(fā)現(xiàn)子線程并沒有中斷
所以 Thread.interrupt()
方法并不能中斷線程,該方法僅僅告訴線程外部已經(jīng)有中斷請(qǐng)求,至于是否中斷還取決于線程自己。在Thread類中除了interrupt()
方法還有另外兩個(gè)非常相似的方法:interrupted
和 isInterrupted
方法,下面來對(duì)這幾個(gè)方法進(jìn)行說明:
-
interrupt
此方法是實(shí)例方法,用于告訴此線程外部有中斷請(qǐng)求,并且將線程中的中斷標(biāo)記設(shè)置為true -
interrupted
此方法是類方法,測(cè)試當(dāng)前線程是否已經(jīng)中斷。線程的中斷狀態(tài) 由該方法清除。換句話說,如果連續(xù)兩次調(diào)用該方法,則第二次調(diào)用將返回 false(在***次調(diào)用已清除了其中斷狀態(tài)之后,且第二次調(diào)用檢驗(yàn)完中斷狀態(tài)前,當(dāng)前線程再次中斷的情況除外)。 -
isInterrupted
此方法是實(shí)例方法測(cè)試線程是否已經(jīng)中斷。線程的中斷狀態(tài) 不受該方法的影響。 線程中斷被忽略,因?yàn)樵谥袛鄷r(shí)不處于活動(dòng)狀態(tài)的線程將由此返回 false 的方法反映出來
處理線程中斷的常用方法
設(shè)置取消標(biāo)記
還是用上面的例子,只不過做了些修改
- public static void main(String[] args) {
- NRunnable run=new NRunnable();
- Thread t=new Thread(run);
- t.start();
- System.out.println("is start.......");
- try {
- Thread.sleep(3000);
- } catch (InterruptedException e) {
- }
- run.cancel();
- System.out.println("cancel ..."+System.currentTimeMillis());
- }
- public static class NRunnable implements Runnable
- {
- public boolean isCancel=false;
- @Override
- public void run() {
- while(!isCancel)
- {
- System.out.println("我沒有種中斷");
- try {
- Thread.sleep(10000);
- } catch (InterruptedException e) {
- }
- }
- System.out.println("我已經(jīng)結(jié)束了..."+System.currentTimeMillis());
- }
- public void cancel()
- {
- this.isCancel=true;
- }
- }
執(zhí)行結(jié)果如下:
is start.......
我沒有種中斷
cancel ...1438396915809
我已經(jīng)結(jié)束了...1438396922809
通過結(jié)果,我們發(fā)現(xiàn)線程確實(shí)已經(jīng)中斷了,但是細(xì)心的同學(xué)應(yīng)該發(fā)現(xiàn)了一個(gè)問題,調(diào)用cancel方法和***線程執(zhí)行完畢之間隔了好幾秒的時(shí)間,也就是說線程不是立馬中斷的,我們下面來分析一下原因:
子線程退出的條件是while循環(huán)結(jié)束,也就是cancel標(biāo)示設(shè)置為true,但是當(dāng)我們調(diào)用cancel方法將calcel標(biāo)記設(shè)置為true 時(shí),while循環(huán)里面有一個(gè)耗時(shí)操作(sleep方法模擬),只有等待耗時(shí)操作執(zhí)行完畢后才會(huì)去檢查這個(gè)標(biāo)記,所以cancel方法和線程退出中間有時(shí) 間間隔。
通過interrupt
和 isinterrupt
方法來中斷線程
- public static void main(String[] args) {
- Thread t=new NThread();
- t.start();
- System.out.println("is start.......");
- try {
- Thread.sleep(3000);
- } catch (InterruptedException e) {
- }
- System.out.println("start interrupt..."+System.currentTimeMillis());
- t.interrupt();
- System.out.println("end interrupt ..."+System.currentTimeMillis());
- }
- public static class NThread extends Thread
- {
- @Override
- public void run() {
- while(!this.isInterrupted())
- {
- System.out.println("我沒有種中斷");
- try {
- Thread.sleep(10000);
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- }
- }
- System.out.println("我已經(jīng)結(jié)束了..."+System.currentTimeMillis());
- }
- }
- }
運(yùn)行結(jié)果如下:
is start.......
我沒有種中斷
start interrupt...1438398800110
我已經(jīng)結(jié)束了...1438398800110
end interrupt ...1438398800110
這次是立馬中斷的,但是這種方法是由局限性的,這種方法僅僅對(duì)于會(huì)拋出InterruptedException
異常的任務(wù)時(shí)有效的,比如java中的sleep、wait
等方法,對(duì)于不會(huì)拋出這種異常的任務(wù)其效果其實(shí)和***種方法是一樣的,都會(huì)有延遲性,這個(gè)例子中還有一個(gè)非常重要的地方就是cache語句中,我們調(diào)用了Thread.currentThread().interrupt()
我們把這句代碼去掉,運(yùn)行你會(huì)發(fā)現(xiàn)這個(gè)線程無法終止,因?yàn)樵趻伋?code>InterruptedException 的同時(shí),線程的中斷標(biāo)志被清除了,所以在while語句中判斷當(dāng)前線程是否中斷時(shí),返回的是false.針對(duì)InterruptedException
異常,我想說的是:一定不能再catch語句塊中什么也不干,如果你實(shí)在不想處理,你可以將異常拋出來,讓調(diào)用拋異常的方法也成為一個(gè)可以拋出InterruptedException
的方法,如果自己要捕獲此異常,那么***在cache語句中調(diào)用 Thread.currentThread().interrupt();
方法來讓高層只要中斷請(qǐng)求并處理該中斷。
對(duì)于上述兩種方法都有其局限性,***種方法只能處理那種工作量不大,會(huì)頻繁檢查循環(huán)標(biāo)志的任務(wù),對(duì)于第二種方法適合用于拋出InterruptedException
的代碼。也就是說***種和第二種方法支持的是支持中斷的線程任務(wù),那么不支持中斷的線程任務(wù)該怎么做呢。
例如 如果一個(gè)線程由于同步進(jìn)行I/O操作導(dǎo)致阻塞,中斷請(qǐng)求不會(huì)拋出InterruptedException
,我們?cè)撊绾沃袛啻司€程呢。
處理不支持中斷的線程中斷的常用方法
改寫線程的interrupt方法
- public static class ReaderThread extends Thread
- {
- public static final int BUFFER_SIZE=512;
- Socket socket;
- InputStream is;
- public ReaderThread(Socket socket) throws IOException
- {
- this.socket=socket;
- is=this.socket.getInputStream();
- }
- @Override
- public void interrupt() {
- try
- {
- socket.close();
- }catch(IOException e)
- {
- }finally
- {
- super.interrupt();
- }
- super.interrupt();
- }
- @Override
- public void run() {
- try
- {
- byte[]buf=new byte[BUFFER_SIZE];
- while(true)
- {
- int count=is.read(buf);
- if(count<0)
- break;
- else if(count>0)
- {
- }
- }
- }catch(IOException e)
- {
- }
- }
- }
- }
例如在上面的例子中,改寫了Thread的interrupt
方法,當(dāng)調(diào)用interrupt
方法時(shí),會(huì)關(guān)閉socket,如果此時(shí)read方法阻塞,那么會(huì)拋出IOException
此時(shí)線程任務(wù)也就結(jié)束了。
以上方法是通過改寫線程的interrupt
方法實(shí)現(xiàn),那么對(duì)于使用線程池的任務(wù)該怎么中斷呢。
改寫線程池的newTaskFor方法
通常我們向線程池中加入一個(gè)任務(wù)采用如下形式:
- Future<?> future=executor.submit(new Runnable(){
- @Override
- public void run() {
- }
- });
- 取消任務(wù)時(shí),調(diào)用的是future的cancel方法,其實(shí)在cancel方法中調(diào)用的是線程的interrupt方法。所以對(duì)于不支持中斷的任務(wù)cancel也是無效的,下面我們看看submit方法里面干了上面吧
- public Future<?> submit(Runnable task) {
- if (task == null) throw new NullPointerException();
- RunnableFuture<Void> ftask = newTaskFor(task, null);
- execute(ftask);
- return ftask;
- }
- 這里調(diào)用的是AbstractExecutorService 的newTaskFor方法,那么我們能不能改寫ThreadPoolExecutor的newTaskFor方法呢,接下來看我在處理吧
- 定義一個(gè)基類,所有需要取消的任務(wù)繼承這個(gè)基類
- public interface CancelableRunnable<T> extends Runnable {
- public void cancel();
- public RunnableFuture<T> newTask();
- }
- 將上面的ReaderThread改為繼承這個(gè)類
- public static class ReaderThread implements CancelableRunnable<Void>
- {
- public static final int BUFFER_SIZE=512;
- Socket socket;
- InputStream is;
- public ReaderThread(Socket socket) throws IOException
- {
- this.socket=socket;
- is=this.socket.getInputStream();
- }
- @Override
- public void run() {
- try
- {
- byte[]buf=new byte[BUFFER_SIZE];
- while(true)
- {
- int count=is.read(buf);
- if(count<0)
- break;
- else if(count>0)
- {
- }
- }
- }catch(IOException e)
- {
- }
- }
- @Override
- public void cancel() {
- try {
- socket.close();
- } catch (IOException e) {
- }
- }
- @Override
- public RunnableFuture<Void> newTask() {
- return new FutureTask<Void>(this,null)
- {
- @Override
- public boolean cancel(boolean mayInterruptIfRunning) {
- return super.cancel(mayInterruptIfRunning);
- if(ReaderThread.this instanceof CancelableRunnable))
- {
- ((CancelableRunnable)(ReaderThread.this)).cancel();
- }else
- {
- super.cancel(mayInterruptIfRunning);
- }
- }
- };
- }
- }
當(dāng)你調(diào)用future的cancel的方法時(shí),它會(huì)關(guān)閉socket,最終導(dǎo)致read方法異常,從而終止線程任務(wù)。