Java 可中斷執行緒

瓜瓜東西發表於2014-07-28

PART.1 無法中斷的執行緒

一個無法中斷的執行緒的例子。

  1. public class UninterruptableThread
  2. {
  3.     @SuppressWarnings("deprecation")
  4.     public static void main(String[] args) throws Exception
  5.     {
  6.         Thread th = new Thread(new TestRunnable());
  7.         
  8.         // 啟動執行緒
  9.         System.out.println("main: start thread.");
  10.         th.start();
  11.         
  12.         // 等待2秒
  13.         Thread.sleep(2000);
  14.         
  15.         // 中斷執行緒
  16.         System.out.println("main: interrupt thread.");
  17.         th.interrupt();
  18.         
  19.         // 等待2秒
  20.         Thread.sleep(2000);
  21.         
  22.         // 停止執行緒
  23.         System.out.println("main: stop thread.");
  24.         th.stop();
  25.     }
  26.     
  27.     private static class TestRunnable implements Runnable
  28.     {
  29.         @Override
  30.         public void run()
  31.         {
  32.             System.out.println("Thread started.");
  33.             while (true)
  34.             {
  35.                 // 避免由於while(true)導致編譯失敗。
  36.                 if (falsebreak;
  37.             }
  38.             
  39.             // 清理工作
  40.             System.out.println("Thread ended.");
  41.         }
  42.     }
  43. }
  • 輸出結果:

main: start thread.
Thread started.
main: interrupt thread.
main: stop thread.

  • Thread物件的interrupt方法僅僅把該執行緒的一個標誌置為true,該方法本身並不包含任何中斷執行緒的操作。
  • stop方法可以將執行緒中止,但通過輸出的結果可以發現,”Thread ended”並沒有被輸出,即執行緒本身不能進行任何清理工作。

PART.2 可中斷的執行緒

  • 執行緒應不斷檢查isInterrupted是否為true,當其返回true時,應開始清理並結束執行緒。(Test1中的while迴圈)
  • Thread.sleep方法會線上程被中斷時丟擲InterruptedException,執行緒可以捕獲該異常並開始清理和結束執行緒。(Test2中的Thread.sleep())
  • 如果迴圈中不時呼叫Thread.sleep,可以處理isInterrupted。

可中斷執行緒的例子:

  1. public class GeneralTest
  2. {
  3.     public static void main(String[] args) throws Exception
  4.     {
  5.         Thread th1 = new Thread(new Test1());
  6.         Thread th2 = new Thread(new Test2());
  7.         
  8.         // 啟動 Test1 和 Test2 執行緒。
  9.         System.out.println("main: starting 'Test1' and 'Test2'.");
  10.         th1.start();
  11.         th2.start();
  12.         
  13.         // 等待3秒。
  14.         System.out.println("main: sleeping for 3 seconds.");
  15.         Thread.sleep(3000);
  16.         
  17.         // 中斷 Test1 和 Test2 執行緒。
  18.         System.out.println("main: interrupting 'Test1' and 'Test2'.");
  19.         th1.interrupt();
  20.         th2.interrupt();
  21.         
  22.         // 等待 Test1 和 Test2 執行緒結束。
  23.         System.out.println("main: waiting for 'Test1' and 'Test2' to end.");
  24.         th1.join();
  25.         th2.join();
  26.         
  27.         System.out.println("main: end.");
  28.     }
  29.     
  30.     private static class Test1 implements Runnable
  31.     {
  32.         @Override
  33.         public void run()
  34.         {
  35.             System.out.println("Test1: start.");
  36.             while (!Thread.currentThread().isInterrupted())
  37.             {
  38.                 // 其他操作...
  39.                 System.out.print("");
  40.             }
  41.             System.out.println("Test1: end.");
  42.         }
  43.     }
  44.     
  45.     private static class Test2 implements Runnable
  46.     {
  47.         @Override
  48.         public void run()
  49.         {
  50.             System.out.println("Test2: start.");
  51.             try
  52.             {
  53.                 while (true)
  54.                 {
  55.                     // 其他操作... 
  56.                     Thread.sleep(1000);
  57.                 }
  58.             }
  59.             catch (InterruptedException e)
  60.             {
  61.                 System.out.println("Test2: InterruptedException");
  62.             }
  63.             System.out.println("Test2: end.");
  64.         }
  65.     }
  66. }

PART.3 isInterrupted()和interrputed()方法的區別

  • isInterrupted方法是例項方法,interrupted方法是靜態方法。

Thread.currentThread().isInterrupted()
Thread.interrupted()

  • interrupted方法在呼叫之後會將中斷標誌置為false。在只對執行緒呼叫一次interrupt的前提下interrupted方法只會返回一次true。
  • 使用interrupted方法判斷應確保在判斷之後開始結束執行緒。

isInterrupted和interrupted方法比較的例子:

  1. public class InterruptedStateTest
  2. {
  3.     public static void main(String[] args) throws Exception
  4.     {
  5.         // "Test1"
  6.         Thread th1 = new Thread(new Test1());
  7.         
  8.         // 啟動 Test1 執行緒,並在3秒之後中斷該執行緒。
  9.         th1.start();
  10.         Thread.yield();
  11.         
  12.         System.out.println("Test1 started... Waiting 3 seconds.");
  13.         Thread.sleep(3000);
  14.         
  15.         System.out.println("Interrupting Test1...");
  16.         th1.interrupt();
  17.         
  18.         Thread.sleep(1000);
  19.         
  20.         System.out.println("---------------------------------------");
  21.                 
  22.         // “Test2"
  23.         Thread th2 = new Thread(new Test2());
  24.         // 啟動 Test2 執行緒,並在3秒之後中斷該執行緒。
  25.         th2.start();
  26.         Thread.yield();
  27.         
  28.         System.out.println("Test2 started... Waiting 3 seconds.");
  29.         Thread.sleep(3000);
  30.         
  31.         System.out.println("Interrupting Test2...");
  32.         th2.interrupt();
  33.         
  34.         Thread.yield();
  35.         
  36.         // 主執行緒結束。
  37.         System.out.println("End of main.");
  38.     }
  39.     
  40.     private static class Test1 implements Runnable
  41.     {
  42.         @Override
  43.         public void run()
  44.         {
  45.             System.out.println("Test1 start...");
  46.             while (true)
  47.             {
  48.                 if (Thread.currentThread().isInterrupted())
  49.                 {
  50.                     if (Thread.currentThread().isInterrupted())
  51.                     {
  52.                         System.out.println("Interrupted...");
  53.                         break;
  54.                     }
  55.                 }
  56.             }
  57.             System.out.println("Test1 end...");
  58.         }
  59.     }
  60.     
  61.     private static class Test2 implements Runnable
  62.     {
  63.         @Override
  64.         public void run()
  65.         {
  66.             // 記錄執行緒開始時間。
  67.             long startTime = System.currentTimeMillis();
  68.             
  69.             System.out.println("Test2 start... " +
  70. "Automatically ends in 6 sec.");
  71.             
  72.             while (true)
  73.             {
  74.                 // 連續判斷2次Thread.interrupted()
  75.                 if (Thread.interrupted())
  76.                 {
  77.                     if (Thread.interrupted())
  78.                     {
  79.                         System.out.println("Interrupted...");
  80.                         break;
  81.                     }
  82.                 }
  83.                 
  84.                 // 如果執行緒2執行超過6秒將自動結束。
  85.                 if (System.currentTimeMillis() - startTime > 6000 )
  86.                 {
  87.                     System.out.println("5 seconds...");
  88.                     break;
  89.                 }
  90.             }
  91.             System.out.println("Test2 end");
  92.         }
  93.     }
  94. }

例子中Test1連續判斷2次Thread.currentThread().isInterrupted(), Test1仍然可以正常中斷。

  1. if (Thread.currentThread().isInterrupted())
  2. {
  3.     if (Thread.currentThread().isInterrupted())
  4.     {
  5.         // 結束執行緒。
  6.     }
  7. }

Test2連續判斷2次Thread.interrupted(),因此Test2執行緒在被呼叫interrupt之後沒有結束。

  1. if (Thread.interrupted())
  2. {
  3.     if (Thread.interrupted())
  4.     {
  5.     // 結束執行緒。
  6.     }
  7. }

PART.4 處理阻塞

阻塞操作如BufferedReader的readLine方法,ServerSocket的accept方法將導致執行緒不能判斷isInterrupted(),因此執行緒中的阻塞不能永久阻塞。處理阻塞的方法有以下:

  • 在呼叫阻塞方法時設定超時時間:

方法

超時後的處理

ReentrantLock

ReadLock

WriteLock

tryLock(long, TimeUnit)

返回false

Condition

await(long, TimeUnit)

awaitNanos(long)

awaitUntil(Date)

返回false

Future

get(long, TimeUnit)

TimeoutException

CyclicBarrier

await(long, TimeUnit)

TimeoutException

CountDownLatch

await(long, TimeUnit)

返回false

Exchanger

exchange(V, long, TimeUnit)

TimeoutException

Semaphore

tryAcquire(long, TimeUnit)

返回false

BlockingQueue<E>

offer(E, long, TimeUnit)

poll(long, TimeUnit)

返回false

返回null

BlockingDeque<E>

offerFirst(E, long, TimeUnit)

offerLast(E, long, TimeUnit)

poolFirst(long, TimeUnit)

poolLast(long, TimeUnit)

返回false

 

返回null

ServerSocket

accept()

通過setSoTimeout設定超時時間。

SocketTimeoutException

  • 該方法在阻塞時如果執行緒被中斷,可以丟擲一個異常:

方法

異常

Thread

sleep(long)

join()

InterruptedException

 

ReentrantLock

ReadLock

WriteLock

lockInterruptibly()

InterruptedException

Condition

await()

InterruptedException

Future

get()

InterruptedException

CyclicBarrier

await()

InterruptedException

CountDownLatch

await()

InterruptedException

Exchanger

exchange(V)

InterruptedException

Semaphore

acquire()

InterruptedException

BlockingQueue<E>

put(E)

take()

InterruptedException

BlockingDeque<E>

putFirst(E)

putLast(E)

takeFirst(E)

takeLast(E)

InterruptedException

  • 呼叫不可中斷阻塞方法的可中斷版本:

阻塞方法

可中斷方法

ReentrantLock

ReadLock

WriteLock

lock()

tryLock()

tryLock(long, TimeUnit)

lockInterruptibly()

Condition

awaitUninterruptibly()

await()

await(long, TimeUnit)

awaitNanos(long)

awaitUntil(Date)

Semaphore

acquireUninterruptibly()

acquire()

tryAcquire()

tryAcquire(long, TimeUnit)

  • 不能設定超時也不能丟擲異常的阻塞方法:
synchronized塊,Object的wait方法。可以使用ReentrantLock和Condition替代。
BufferedReader的readLine方法,ObjectInputStream得readObject方法。(如果底層流是通過Socket獲得,可以通過Socket設定超時)

PART.5 處理Thread.sleep()

1. 捕獲異常並結束執行緒

  • 捕獲InterruptedException異常,開始清理操作並結束執行緒。
  1. public void run()
  2. {
  3.     try
  4.     {
  5.         while (true)
  6.         {
  7.             // 需要進行的操作
  8.             Thread.sleep(500);
  9.         }
  10.     }
  11.     catch (InterruptedException e)
  12.     {
  13.     }
  14.     
  15.     // 清理操作
  16. }

2. 捕獲異常,再次呼叫interrupt

  1. public void run()
  2. {
  3.     while (!Thread.currentThread().isInterrupted())
  4.     {
  5.         try
  6.         {
  7.             // 需要進行的操作 1
  8.             Thread.sleep(500);
  9.         }
  10.         catch (InterruptedException e)
  11.         {
  12.             // 再次呼叫interrupt
  13.             Thread.currentThread().interrupt();
  14.         }
  15.         
  16.         try
  17.         {
  18.             // 需要進行的操作 2
  19.             Thread.sleep(500);
  20.         }
  21.         catch (InterruptedException e)
  22.         {
  23.             // 再次呼叫interrupt
  24.             Thread.currentThread().interrupt();
  25.         }
  26.     }
  27.     
  28.     // 清理操作
  29. }

PART.6 處理ReentrantLock和Condition

1. 通過lockInterruptibly方法中斷

  • 捕獲lockInterruptibly方法可能跑出的InterruptedException,並結束執行緒。
  1. public void run()
  2. {
  3.     try
  4.     {
  5.         while (true)
  6.         {
  7.             locker.lockInterruptibly();
  8.             // 其他操作
  9.             locker.unlock();
  10.         }
  11.     }
  12.     catch (InterruptedException e)
  13.     {
  14.     }
  15.     // 清理操作
  16. }

2. 通過不設定超時的tryLock方法中斷

  • tryLock方法將不阻塞。
  • 通過捕獲Thread.sleep的異常(或其他方法)中斷執行緒。
  1. public void run()
  2. {
  3.     try
  4.     {
  5.         while (true)
  6.         {
  7.             if (locker.tryLock())
  8.             {
  9.                 // 其他操作
  10.             }
  11.             
  12.             Thread.sleep(500);
  13.         }
  14.     }
  15.     catch (InterruptedException e)
  16.     {
  17.     }
  18. }

3. 通過設定超時的tryLock方法中斷

  • 捕獲tryLock方法線上程中斷時丟擲的InterrupedException並結束執行緒。
  1. public void run()
  2. {
  3.     try
  4.     {
  5.         while (true)
  6.         {
  7.             if (locker.tryLock(1, TimeUnit.SECONDS))
  8.             {
  9.                 // 其他操作
  10.             }
  11.         }
  12.     }
  13.     catch (InterruptedException e)
  14.     {
  15.     }
  16.     // 清理操作
  17. }
4. 通過捕獲Conditon的await方法丟擲的異常中斷
  • 捕獲Condition的await方法丟擲的異常並結束執行緒。
  1. public void run()
  2. {
  3.     try
  4.     {
  5.         while (true)
  6.         {
  7.             locker.lockInterruptibly();
  8.             condition.await();
  9.             // 其他操作
  10.             locker.unlock();
  11.         }
  12.     }
  13.     catch (InterruptedException e)
  14.     {
  15.     }
  16.     // 清理操作
  17. }

5. 可中斷的Producer和Consumer示例

  • produce方法線上程中斷時將跑出InterruptedException。
  • run方法捕獲該異常,並中斷執行緒。
  1. public void run()
  2. {
  3.     try
  4.     {
  5.         while (true)
  6.         {
  7.             produce();
  8.             Thread.sleep((int)(Math.random() * 3000));
  9.         }
  10.     }
  11.     catch (InterruptedException e)
  12.     {
  13.     }
  14.     System.out.println("producer: end.");
  15. }
  16. private void produce() throws InterruptedException
  17. {
  18.     locker.lockInterruptibly();
  19.     try
  20.     {
  21.         // Produce
  22.         int x = (int)(Math.random() * 100);
  23.         queue.offer(x);
  24.         emptyCondition.signalAll();
  25.         System.out.printf("producer: %d and signal all. queue = %d /n",
  26.                 x, queue.size());
  27.     }
  28.     finally
  29.     {
  30.         locker.unlock();
  31.     }
  32. }
  • Consumer執行緒被中斷後不結束,直到佇列內所有資料被輸出。
  • 不使用Thread.currentThread().isInterrupted()而定義isInt記錄中斷,可以避免中斷導致ReentrantLock方法的tryLock不能獲得鎖而直接丟擲異常。
  1. public void run()
  2. {
  3.     // 執行緒是否被中斷
  4.     boolean isInt = false;
  5.     
  6.     // 執行緒中斷後,將佇列內的資料全部讀出,再結束執行緒。
  7.     while (!isInt || !queue.isEmpty())
  8.     {
  9.         try
  10.         {
  11.             consume();
  12.             Thread.sleep((int)(Math.random() * 3000));
  13.         }
  14.         catch (InterruptedException e)
  15.         {
  16.             isInt = true;
  17.         }
  18.     }
  19.     System.out.println("consumer: end.");
  20. }
  21. private void consume() throws InterruptedException
  22. {
  23.     if (!locker.tryLock(5, TimeUnit.SECONDS))
  24.     {
  25.         // 沒有獲得鎖,不進行任何操作。 避免死鎖。
  26.         return;
  27.     }
  28.     
  29.     try
  30.     {
  31.         // Consume
  32.         while (queue.isEmpty())
  33.         {
  34.             System.out.println("consumer: waiting for condition.");
  35.             if (!emptyCondition.await(5, TimeUnit.SECONDS))
  36.             {
  37.                 // 5秒沒有等待到條件,不進行任何操作。
  38.                 // 避免中斷後在此處等待。
  39.                 return;
  40.             }
  41.         }
  42.         int x = queue.poll();
  43.         System.out.printf("consumer: %d, queue=%d /n", x, queue.size());
  44.     }
  45.     finally
  46.     {
  47.         locker.unlock();
  48.     }
  49. }
PART.7 處理BlockingQueue
  • 使用BlockingQueue處理Consumer和Producer問題。
  • put和take方法可以線上程被中斷時丟擲InterruptedException。

Producer:

  1. public void run()
  2. {
  3.     try
  4.     {
  5.         while (true)
  6.         {
  7.             int x = (int)(Math.random() * 100);
  8.             queue.put(x);
  9.             System.out.println("producer: " + x);
  10.             Thread.sleep((int)(Math.random() * 3000));
  11.         }
  12.     }
  13.     catch (InterruptedException e)
  14.     {
  15.     }
  16.     System.out.println("producer: end.");
  17. }

Consumer:

  1. public void run()
  2. {
  3.     try
  4.     {
  5.         while (true)
  6.         {
  7.             int x = queue.take();
  8.             System.out.println("consumer: " + x);
  9.             Thread.sleep((int)(Math.random() * 3000));
  10.         }
  11.     }
  12.     catch (InterruptedException e)
  13.     {
  14.     }
  15.     System.out.println("consumer: end.");
  16. }

PART.8 處理Socket和ServerSocket

1. 處理ServerSocket的accept方法

呼叫ServerSocket的setSoTimeout方法設定超時時間。捕獲並處理超時引起的SocketTimeoutException。不需要處理該異常。
  1. public void run()
  2. {
  3.     try
  4.     {
  5.         // 設定超時時間
  6.         serverSocket.setSoTimeout(2000);
  7.     }
  8.     catch (SocketException e)
  9.     {
  10.         e.printStackTrace();
  11.         System.exit(-1);
  12.     }
  13.     
  14.     while (!Thread.currentThread().isInterrupted())
  15.     {
  16.         try
  17.         {
  18.             @SuppressWarnings("unused")
  19.             Socket s = serverSocket.accept();
  20.         }
  21.         catch (SocketTimeoutException e)
  22.         {
  23.             // 超時,不進行任何處理,再次呼叫accept方法
  24.         }
  25.         catch (IOException e)
  26.         {
  27.             e.printStackTrace();
  28.         }
  29.     }
  30.     // 清理工作
  31. }

2. 處理包裝自Socket的BufferedReader的readLine方法

  • 呼叫Socket的setSoTimeout方法設定超時時間。
  • BufferedReader的readLine方法在阻塞指定的時間後將丟擲SocketTimeoutException。
  • 不需要處理該異常。
  1. public void run()
  2. {
  3.     BufferedReader reader = null;
  4.     
  5.     try
  6.     {
  7.         // 建立測試用的連結
  8.         serverSocket = new ServerSocket(10009);
  9.         socket = new Socket("127.0.0.1"10009);
  10.         
  11.         Thread.sleep(500);
  12.         Socket s = serverSocket.accept();
  13.         
  14.         // 向socket傳送5行資料
  15.         OutputStreamWriter w = new OutputStreamWriter(
  16.                 s.getOutputStream());
  17.         w.write("line1 /n line2 /n line3 /n line4 /n line5 /n");
  18.         w.flush();
  19.         
  20.         // 設定超時
  21.         socket.setSoTimeout(1000);
  22.         // 建立BufferedReader
  23.         reader = new BufferedReader(
  24.                 new InputStreamReader(socket.getInputStream()));
  25.     }
  26.     catch (IOException e)
  27.     {
  28.         e.printStackTrace();
  29.         System.exit(-1);
  30.     }
  31.     catch (InterruptedException e)
  32.     {
  33.         e.printStackTrace();
  34.         System.exit(-1);
  35.     }
  36.     
  37.     while (!Thread.currentThread().isInterrupted())
  38.     {
  39.         try
  40.         {
  41.             String s = reader.readLine();
  42.             
  43.             if (s == null)
  44.             {
  45.                 // 流結束
  46.                 break;
  47.             }
  48.             
  49.             // 輸出讀取的資料
  50.             System.out.println("thread: " + s);
  51.         }
  52.         catch (SocketTimeoutException e)
  53.         {
  54.             System.out.println("thread: socket timeout.");
  55.         }
  56.         catch (IOException e)
  57.         {
  58.             e.printStackTrace();
  59.         }
  60.     }
  61.     
  62.     // 清理
  63.     try { serverSocket.close(); } catch (Exception e) { }
  64.     try { socket.close(); } catch (Exception e) { }
  65.     System.out.println("thread: end.");
  66. }

3. 處理包裝自Socket的ObjectInputStream的readObject方法

  • 與readLine的處理方法類似。
  • 呼叫Socket的setSoTimeout方法設定超時時間。
  • ObjectInputStream的readObject方法在阻塞指定的時間後將丟擲異常。
  • 不需要處理該異常。
  1. public void run()
  2. {
  3.     ObjectInputStream ois = null;
  4.     try
  5.     {
  6.         // 建立測試用的連結
  7.         serverSocket = new ServerSocket(10009);
  8.         socket = new Socket("127.0.0.1"10009);
  9.         Thread.sleep(500);
  10.         Socket s = serverSocket.accept();
  11.         // 向socket傳送3個物件
  12.         ObjectOutputStream oos = new ObjectOutputStream(
  13.                 s.getOutputStream());
  14.         oos.writeObject(new TestData("object 1"));
  15.         oos.writeObject(new TestData("object 2"));
  16.         oos.writeObject(new TestData("object 3"));
  17.         // 設定超時
  18.         socket.setSoTimeout(1000);
  19.         // 建立ObjectInputStream
  20.         ois = new ObjectInputStream(socket.getInputStream());
  21.     }
  22.     catch (IOException e)
  23.     {
  24.         e.printStackTrace();
  25.         System.exit(-1);
  26.     }
  27.     catch (InterruptedException e)
  28.     {
  29.         e.printStackTrace();
  30.         System.exit(-1);
  31.     }
  32.     while (!Thread.currentThread().isInterrupted())
  33.     {
  34.         try
  35.         {
  36.             TestData s = (TestData)ois.readObject();
  37.             if (s == null)
  38.             {
  39.                 // 流結束
  40.                 break;
  41.             }
  42.             // 輸出讀取的資料
  43.             System.out.println("thread: " + s.data);
  44.         }
  45.         catch (SocketTimeoutException e)
  46.         {
  47.             System.out.println("thread: socket timeout.");
  48.         }
  49.         catch (IOException e)
  50.         {
  51.             e.printStackTrace();
  52.         }
  53.         catch (ClassNotFoundException e)
  54.         {
  55.             e.printStackTrace();
  56.         }
  57.     }
  58.     // 清理
  59.     try { serverSocket.close(); } catch (Exception e) { }
  60.     try { socket.close(); } catch (Exception e) { }
  61.     System.out.println("thread: end.");
  62. }

其中,TestData是一個簡單的可序列化的類。

  1. private static class TestData implements Serializable
  2. {
  3.     private static final long serialVersionUID = 6147773210845607198L;
  4.     public String data;
  5.     public TestData(String data)
  6.     {
  7.         this.data = data;
  8.     }
  9. }

 

PART.9 總結

見“PART.2可中斷的執行緒”和“PART.4 處理阻塞”。

相關文章