線程的狀態
- 新建(new):當線程被創建時,它只會短時間處於這種狀態。它已經分配了必要的系統資源,完成了初始化。之后線程調度器將把這個線程轉變為可運行或者阻塞狀態;
- 就緒(Runnable):在這種狀態下,只要調度器分配時間片給線程,線程就可以運行了;
- 阻塞(Blocked):有某個條件阻止線程運行,調度器將忽略阻塞狀態的線程,不會分配時間片給它,直到線程進入就緒狀態,它才有可能執行;
- 死亡(Dead):處於死亡或者終結狀態的線程將不再是可調度的,並且也不會被分配到時間片。任務死亡的方式通常是從run方法返回,或者被中斷;
下面列舉線程進入阻塞狀態的幾個可能原因:
- 通過調用sleep(mils)使任務進入指定時間的睡眠狀態;
- 調用wait使線程掛起,直到線程得到notify或者notifyAll信息,線程才會進入就緒狀態;
- 任務在等待輸入輸出完成;
- 任務試圖調用某個類/對象的同步方法,但是對象鎖不可用;
由於處於阻塞狀態的線程被掛起,得不到執行,即是代碼中有判斷狀態值某一點而退出,亦不會到達該點,這種情況下應該強制任務跳出阻塞狀態。
中斷
Thread類包含interrupt方法,用於終止被阻塞的任務。調用interrupt方法將設置線程的中斷狀態,如果一個線程已經被阻塞或者試圖執行一個阻塞操作,那么設置線程的中斷狀態將拋出InterruptedException異常。當拋出InterruptedException異常或者該任務調用Thread.interrupted()時,線程中斷狀態將被重置。這個中斷被阻塞的線程需要我們持有線程對象。
Java SE5起建議使用Executor來創建並發任務,當在Executor上調用shutdownNow(),那么它將發生一個interrupt給它啟動的所有線程。如果只是想關閉某個特定而不是全部的任務,要通過submit來提交任務,返回Future<?>對象,在Future對象上調用cancel來中斷線程,也可以傳遞true給cancel,那么Future對象就會擁有在該線程上調用interrupt以終止線程的權限。
下面看看不同阻塞條件下線程中斷情況:
//定義sleep進入阻塞的任務
public class SleepBlocked implements Runnable{ public void run() { try { TimeUnit.SECONDS.sleep(10); }catch (InterruptedException e) { System.out.println("interrupt from sleep..."); } System.out.println("exiting from SleepBlocked run"); } } //定義因為IO進入阻塞的任務 public class IOBlocked implements Runnable { InputStream input; public IOBlocked(InputStream input) { this.input = input; } public void run() { try { System.out.println("waiting for input..."); input.read(); }catch (IOException e) { System.out.println("IOException..."); if(Thread.interrupted()) { System.out.println("interrupt from IOBlocked..."); }else { throw new RuntimeException(); } } System.out.println("exiting from IOBlocked......"); } } //定義因為嘗試獲取對象鎖進入阻塞的任務 public class SynchronizedBlocked implements Runnable{ public synchronized void f() { while (true) Thread.yield(); } public SynchronizedBlocked() { new Thread() { public void run() { f(); } }.start(); } public void run() { System.out.println("trying to call f()..."); f(); System.out.println("exiting from SynchronizedBlocked...."); } }
測試類如下:
public class InterruptedTest { private static ExecutorService executor = Executors.newCachedThreadPool(); static void test(Runnable task) throws InterruptedException{ //使用Executor的submit方法提交任務,或者線程的參考,以便中斷線程
Future future = executor.submit(task); TimeUnit.SECONDS.sleep(1); System.out.println("interruptting "+task.getClass().getName());
//通過傳遞true給Future對象的cancel方法,運行中斷被阻塞的線程 future.cancel(true); System.out.println("interrupt send to "+task.getClass().getName()); } public static void main(String[] args) throws Exception{ test(new SleepBlocked()); test(new IOBlocked(System.in)); test(new SynchronizedBlocked()); } }
輸出:
interruptting com.thread.test.SleepBlocked
interrupt send to com.thread.test.SleepBlocked
interrupt from sleep... //因為睡眠而阻塞的線程被中斷
exiting from SleepBlocked run
//因為IO而阻塞的線程沒有被中斷
waiting for input...
interruptting com.thread.test.IOBlocked
interrupt send to com.thread.test.IOBlocked
//因為獲取對象鎖而阻塞的線程沒有被中斷
trying to call f()...
interruptting com.thread.test.SynchronizedBlocked
interrupt send to com.thread.test.SynchronizedBlocked
exiting from IOBlocked......
從輸出可以看出,可以中斷對sleep的調用,但是不能中斷試圖獲取對象鎖(synchronized實現)或者執行IO任務的線程。
實現嘗試獲取對象鎖的阻塞中斷
從上面可以知道,以synchronized關鍵字實現的同步對象鎖在阻塞時不能被中斷。這里介紹一種既可以提供鎖功能,又能夠在阻塞時被中斷的實現,那就是使用ReentrantLock。
public class BlockedMutex{ private Lock lock = new ReentrantLock(); public void f() { try {
//一直占有鎖,除非被中斷 lock.lockInterruptibly(); }catch (InterruptedException e) { System.out.println(Thread.currentThread().getName()+" interrupted from lock acquisition in f()"); } } } public class LockBlocked implements Runnable { private BlockedMutex blockedMutex; public LockBlocked(BlockedMutex blockedMutex) { this.blockedMutex = blockedMutex; } public void run() { System.out.println(Thread.currentThread().getName()+" waiting for blockedMutex...."); blockedMutex.f(); System.out.println(Thread.currentThread().getName()+" Broken out of blocked call"); } } public class InterruptedTest {public static void main(String[] args) throws Exception{ BlockedMutex blockedMutex = new BlockedMutex(); Thread t1 = new Thread(new LockBlocked(blockedMutex)); Thread t2 = new Thread(new LockBlocked(blockedMutex)); t1.start(); t2.start(); t2.interrupt(); } }
輸出:
Thread-0 waiting for blockedMutex....
Thread-1 waiting for blockedMutex....
Thread-0 Broken out of blocked call
Thread-1 interrupted from lock acquisition in f()
Thread-1 Broken out of blocked call
兩個任務都是使用同一個BlockedMutex實例的f方法,f方法里面是以永久占有鎖的方式獲取對象鎖,這樣只有最先被執行的任務能夠占有鎖,之后的任務將一直等待。測試中見到t1占有鎖,t2在等待鎖,中斷t2,看到t2驅動的任務里面拋出InterruptedException,這也說明ReentrantLock鎖確實能夠在阻塞下被中斷。
實現IO操作的阻塞中斷
檢查中斷
從上面的學習得知,在線程進入阻塞或者要進入阻塞時,調用線程的interrupt,線程將會拋出異常(IO阻塞或者synchronized阻塞例外,不能被中斷)。在任務里面,並不都是可能導致線程進入阻塞的代碼,在任務執行不會導致阻塞的代碼時,可以通過調用Thread的interrupted方法來檢查中斷狀態,因為Thread的interrupt方法會設置線程的中斷標志位,而interrupted方法則能夠讀取到該標志位,並且重置標志位。請看下面例子:
public class InterruptedCheck implements Runnable { private double d = 1d; public void run() { try { while (!Thread.interrupted()) { System.out.println("sleeping...");
//線程睡眠2s,進入阻塞,如果在睡眠時間interrupt,將會拋出異常 Thread.sleep(2000); System.out.println("calculating....");
//這里以運算模擬任務,要有一定的時間,但是線程不會進入阻塞狀態。如果在運算期間進行interrupt,將不會拋出異常,且中斷標志位被設置 for(int i=1;i<25000000;i++) { d = d + (Math.PI + Math.E) /d; } } System.out.println("detected interrupted, not from blocked..."); }catch (InterruptedException e) { System.out.println("interrupted from blocked..."); } } } public static void main(String[] args) throws Exception{ Thread thread = new Thread(new InterruptedCheck()); thread.start();
//這里man的睡眠時間為不同的值,就可以在thread處於不同的狀態(sleep進入阻塞或者正常執行運算)interrupt TimeUnit.MILLISECONDS.sleep(2100); System.out.println("interruptting..."); thread.interrupt(); } 輸出:main的sleep時間為1500milsec時 sleeping... interruptting... interrupted from blocked...
main的sleep時間為2100milsec時
sleeping...
calculating....
interruptting...
detected interrupted, not from blocked...
可以看到,在可以被中斷的阻塞狀態下中斷線程,將會以拋出異常的形式退出任務。在運行狀態下中斷線程,被中斷的線程的中斷標志位被設置,通過interrupted方法可以讀取到線程該標志位,從而判斷線程是否被中斷,進而執行退出任務的策略判斷。