深入分析Java執行緒中斷機制

yuanzeyao發表於2015-08-01

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) {

        }
      }
    }

  }
}

如果interrutp方法能夠中斷執行緒,那麼在列印了is interrupt…….之後應該是沒有log了,我們看看執行結果吧

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

通過結果可以發現子執行緒並沒有中斷

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

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

處理執行緒中斷的常用方法

設定取消標記

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

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

  }

執行結果如下:

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

  }
}

執行結果如下:

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)
     {

     }
  }
 }
}

例如在上面的例子中,改寫了Thread的interrupt 方法,當呼叫interrupt 方法時,會關閉socket,如果此時read方法阻塞,那麼會丟擲IOException 此時執行緒任務也就結束了。

以上方法是通過改寫執行緒的interrupt 方法實現,那麼對於使用執行緒池的任務該怎麼中斷呢。

改寫執行緒池的newTaskFor方法

通常我們向執行緒池中加入一個任務採用如下形式:

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

      }
    });

取消任務時,呼叫的是future的cancel方法,其實在cancel方法中呼叫的是執行緒的interrupt方法。所以對於不支援中斷的任務cancel也是無效的,下面我們看看submit方法裡面幹了上面吧

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

}

將上面的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);
              }
            }
          };

    }
 }

當你呼叫future的cancel的方法時,它會關閉socket,最終導致read方法異常,從而終止執行緒任務。

相關文章