深入分析Java線程中斷機制

jopen 9年前發布 | 9K 次閱讀 Java Java開發

Thread.interrupt真的能中斷線程嗎

在平時的開發過程中,相信都會使用到多線程,在使用多線程時,大家也會遇到各種各樣的問題,今天我們就來說說一個多線程的問題——線程中斷。在 java中啟動線程非常容易,大多數情況下我是讓一個線程執行完自己的任務然后自己停掉,但是有時候我們需要取消某個操作,比如你在網絡下載時,有時候需要取消下載。實現線程的安全中斷并不是一件容易的事情,因為Java并不支持安全快速中斷線程的機制,這里估計很多同學就會說了,java不是提供了Thread.interrupt 方法中斷線程嗎,好吧,我們今天就從這個方法開始說起。

但是調用此方法線程真的會停止嗎?我們寫個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) {

    }
  }
}

} }</pre>

如果interrutp方法能夠中斷線程,那么在打印了is interrupt…….之后應該是沒有log了,我們看看執行結果吧

is start.......
我沒有種中斷
我沒有種中斷
我沒有種中斷
我沒有種中斷
我沒有種中斷
is interrupt.......
我沒有種中斷
我沒有種中斷
我沒有種中斷
我沒有種中斷
我沒有種中斷
....

通過結果可以發現子線程并沒有中斷

所以 Thread.interrupt() 方法并不能中斷線程,該方法僅僅告訴線程外部已經有中斷請求,至于是否中斷還取決于線程自己。在Thread類中除了interrupt() 方法還有另外兩個非常相似的方法:interrupted 和 isInterrupted 方法,下面來對這幾個方法進行說明:

  • interrupt 此方法是實例方法,用于告訴此線程外部有中斷請求,并且將線程中的中斷標記設置為true
  • interrupted 此方法是類方法,測試當前線程是否已經中斷。線程的中斷狀態 由該方法清除。換句話說,如果連續兩次調用該方法,則第二次調用將返回 false(在第一次調用已清除了其中斷狀態之后,且第二次調用檢驗完中斷狀態前,當前線程再次中斷的情況除外)。
  • isInterrupted 此方法是實例方法測試線程是否已經中斷。線程的中斷狀態 不受該方法的影響。 線程中斷被忽略,因為在中斷時不處于活動狀態的線程將由此返回 false 的方法反映出來
  • </ul>

    處理線程中斷的常用方法

    設置取消標記

    還是用上面的例子,只不過做了些修改

    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("我已經結束了..."+System.currentTimeMillis());
    }
    
    public void cancel()
    {
      this.isCancel=true;
    }
    
    

    }</pre>

    執行結果如下:

    is start.......
    我沒有種中斷
    cancel ...1438396915809
    我已經結束了...1438396922809

    通過結果,我們發現線程確實已經中斷了,但是細心的同學應該發現了一個問題,調用cancel方法和最后線程執行完畢之間隔了好幾秒的時間,也就是說線程不是立馬中斷的,我們下面來分析一下原因:

    子線程退出的條件是while循環結束,也就是cancel標示設置為true,但是當我們調用cancel方法將calcel標記設置為true 時,while循環里面有一個耗時操作(sleep方法模擬),只有等待耗時操作執行完畢后才會去檢查這個標記,所以cancel方法和線程退出中間有時間間隔。

    通過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("我已經結束了..."+System.currentTimeMillis());
    }
    
    

    } }</pre>

    運行結果如下:

    is start.......
    我沒有種中斷
    start interrupt...1438398800110
    我已經結束了...1438398800110
    end interrupt ...1438398800110

    這次是立馬中斷的,但是這種方法是由局限性的,這種方法僅僅對于會拋出InterruptedException 異常的任務時有效的,比如java中的sleep、wait 等方法,對于不會拋出這種異常的任務其效果其實和第一種方法是一樣的,都會有延遲性,這個例子中還有一個非常重要的地方就是cache語句中,我們調用了Thread.currentThread().interrupt() 我們把這句代碼去掉,運行你會發現這個線程無法終止,因為在拋出InterruptedException 的同時,線程的中斷標志被清除了,所以在while語句中判斷當前線程是否中斷時,返回的是false.針對InterruptedException 異常,我想說的是:一定不能再catch語句塊中什么也不干,如果你實在不想處理,你可以將異常拋出來,讓調用拋異常的方法也成為一個可以拋出InterruptedException 的方法,如果自己要捕獲此異常,那么最好在cache語句中調用 Thread.currentThread().interrupt(); 方法來讓高層只要中斷請求并處理該中斷。

    對于上述兩種方法都有其局限性,第一種方法只能處理那種工作量不大,會頻繁檢查循環標志的任務,對于第二種方法適合用于拋出InterruptedException的代碼。也就是說第一種和第二種方法支持的是支持中斷的線程任務,那么不支持中斷的線程任務該怎么做呢。

    例如 如果一個線程由于同步進行I/O操作導致阻塞,中斷請求不會拋出InterruptedException,我們該如何中斷此線程呢。

    處理不支持中斷的線程中斷的常用方法

    改寫線程的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)
     {
    
     }
    

    } } }</pre>

    例如在上面的例子中,改寫了Thread的interrupt 方法,當調用interrupt 方法時,會關閉socket,如果此時read方法阻塞,那么會拋出IOException 此時線程任務也就結束了。

    以上方法是通過改寫線程的interrupt 方法實現,那么對于使用線程池的任務該怎么中斷呢。

    改寫線程池的newTaskFor方法

    通常我們向線程池中加入一個任務采用如下形式:

    Future<?> future=executor.submit(new Runnable(){
          @Override
          public void run() {

      }
    });</pre> <p>取消任務時,調用的是future的cancel方法,其實在cancel方法中調用的是線程的interrupt方法。所以對于不支持中斷的任務cancel也是無效的,下面我們看看submit方法里面干了上面吧 </p>
    

        public Future<?> submit(Runnable task) {
            if (task == null) throw new NullPointerException();
            RunnableFuture<Void> ftask = newTaskFor(task, null);
            execute(ftask);
            return ftask;
        }

    這里調用的是AbstractExecutorService 的newTaskFor方法,那么我們能不能改寫ThreadPoolExecutor的newTaskFor方法呢,接下來看我在處理吧

    定義一個基類,所有需要取消的任務繼承這個基類

    public interface CancelableRunnable<T> extends Runnable {

    public void cancel(); public RunnableFuture<T> newTask();

    }</pre>

    將上面的ReaderThread改為繼承這個類

     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);
              }
            }
          };
    
    }
    

    }</pre>

    當你調用future的cancel的方法時,它會關閉socket,最終導致read方法異常,從而終止線程任務。

     來源:yuanzeyao

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!