Java執行緒簡介
本文將介紹Java執行緒的狀態、執行緒的中斷、執行緒間通訊和執行緒的實現。
執行緒的狀態
Java語言定義了6種不同的執行緒狀態,在給定的一個時刻,執行緒只能處於其中的一個狀態。

執行緒狀態
執行緒狀態的轉換關係,如下圖所示:

執行緒的中斷
當希望終止一個執行緒時,並不是簡單的呼叫 stop 命令。雖然 api 仍然可以呼叫,但是和其他的執行緒控制方法如 suspend、resume 一樣都是過期了的不建議使用的方法。就拿 stop 來說,stop 方法在結束一個執行緒時並不會保證執行緒的資源正常釋放,因此會導致程式可能出現一些不確定的狀態。要優雅的終止一個執行緒,可以使用中斷的方式。
中斷可以理解為執行緒的一個標識位屬性,它表示一個執行中的執行緒是否被其他執行緒進行了中斷操作。中斷好比其他執行緒對該執行緒打了個招呼,其他執行緒通過呼叫該執行緒的interrupt()方法對其進行中斷操作。
執行緒通過檢查自身是否被中斷來進行響應,執行緒通過方法isInterrupted()來進行判斷是否被中斷,也可以呼叫靜態方法Thread.interrupted()對當前執行緒的中斷標識位進行復位。
另外許多宣告丟擲InterruptedException的方法(例如Thread.sleep(long millis)方法)這些方法在丟擲InterruptedException之前,Java虛擬機器會先將該執行緒的中斷標識位清除,然後丟擲InterruptedException,此時呼叫isInterrupted()方法將會返回false。
public void Thread.interrupt() //中斷執行緒 public boolean Thread.isInterrupted() //判斷是否被中斷 public static boolean Thread.interrupted() //判斷是否被中斷,並清除當前中斷狀態
通過下面這個例子,可實現執行緒終止的邏輯
//通過中斷的方式 public class InterruptDemo { private static int i; public static void main(String[] args) throws InterruptedException { Thread thread=new Thread(()->{ while(!Thread.currentThread().isInterrupted()){ i++; } System.out.println("Num:"+i); },"interruptDemo"); thread.start(); TimeUnit.SECONDS.sleep(1); thread.interrupt(); }} //通過標記符的方式 public class VolatileDemo { private volatile static boolean stop=false; public static void main(String[] args) throws InterruptedException { Thread thread=new Thread(()->{ int i=0; while(!stop){ i++; } }); thread.start(); System.out.println("begin start thread"); Thread.sleep(1000); stop=true; } }
這種通過識別符號或者中斷操作的方式能夠使執行緒在終止時有機會去清理資源,而不是武斷地將執行緒停止,因此這種終止執行緒的做法顯得更加安全和優雅。
執行緒的通訊
為了支援多執行緒之間的協作,JDK提供了兩個非常重要的方法 wait() 和 notify() 。這兩個方法並不是在Thread類中,而是Object類。這也意味著任何物件都可以呼叫這兩個方法。
這兩個方法的簽名如下,都是本地方法:
public final native void wait(long timeout) throws InterruptedException; public final native void notify();
呼叫wait()方法,首先會獲取監視器鎖,獲得成功以後,會讓當前執行緒進入等待狀態進入等待佇列並且釋放鎖;然後當其他執行緒呼叫notify()或者notifyAll()以後,會選擇從等待佇列中喚醒任意一個執行緒,而執行完notify()方法以後,並不會立馬喚醒執行緒,原因是當前的執行緒仍然持有這把鎖,處於等待狀態的執行緒無法獲得鎖。必須要等到當前的執行緒執行完按monitorexit指令以後,也就是鎖被釋放以後,處於等待佇列中的執行緒就可以開始競爭鎖。競爭是不公平的,並不是先等待的執行緒就會被優先選擇。

public class ThreadWait extends Thread{ private Object lock; public ThreadWait(Object lock) { this.lock = lock; } @Override public void run() { synchronized (lock){ System.out.println("開始執行 thread wait"); try { lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("執行結束 thread wait"); } } } public class ThreadNotify extends Thread{ private Object lock; public ThreadNotify(Object lock) { this.lock = lock; } @Override public void run() { synchronized (lock){ System.out.println("開始執行 thread notify"); lock.notify(); System.out.println("執行結束 thread notify"); } } }
注:因為在執行wait()和notify()時都必須獲取到鎖,所以wait和notify都需要在synchronized裡面。wait()與sleep()的區別,sleep()不會釋放釋放鎖。
join():在很多情況下,一個執行緒的輸入可能非常依賴於另外一個或者多個執行緒的輸出,此時,這個執行緒就需要等待依賴執行緒執行完畢,才能繼續執行。JDK提供了 join() 操作來實現這個功能。執行緒Thread除了提供join()方法之外,還提供了join(long millis)和join(long millis,int nanos)兩個具備超時特性的方法。這兩個超時方法表示,如果執行緒thread在給定的超時時間裡沒有終止,那麼將會從該超時方法中返回。例如下面的demo,建立了10個執行緒,編號0~9,每個執行緒呼叫前一個執行緒的join()方法。
public class Join { public static void main(String[] args) throws Exception { Thread previous = Thread.currentThread(); for (int i = 0; i < 10; i++) { // 每個執行緒擁有前一個執行緒的引用,需要等待前一個執行緒終止,才能從等待中返回 Thread thread = new Thread(new Domino(previous), String.valueOf(i)); thread.start(); previous = thread; } TimeUnit.SECONDS.sleep(5); System.out.println(Thread.currentThread().getName() + " terminate."); } static class Domino implements Runnable { private Thread thread; public Domino(Thread thread) { this.thread = thread; } public void run() { try { thread.join(); } catch (InterruptedException e) { } System.out.println(Thread.currentThread().getName() + " terminate."); } } } //輸出如下: main terminate. 0 terminate. 1 terminate. 2 terminate. 3 terminate. 4 terminate. 5 terminate. 6 terminate. 7 terminate. 8 terminate. 9 terminate.
yield():表示當前執行緒讓出CPU,當前執行緒讓出CPU後,還會進行CPU資源的爭奪。
執行緒的實現
我們注意到Thread類和大部分的Java API有顯著的差別,它的所有關鍵方法都是宣告為Native的。一個Native方法往往意味著這個方法沒有使用或無法使用平臺無關的手段來實現(當然也可能是為了執行效率而使用Native方法,不過,通常最高效率的手段也就是平臺相關的手段)。
實現執行緒主要有3種方式:使用核心執行緒實現、使用使用者執行緒實現和使用使用者執行緒加輕量級程序混合實現。
使用核心執行緒(Kernel-Level Thread,KLT)實現:就是直接由作業系統核心支援的執行緒,由核心完成執行緒切換,核心通過操縱排程器進行執行緒排程,並且負責將執行緒的任務對映到各個處理器上。程式一般不會直接使用核心執行緒,而是去使用核心執行緒的一種高階介面——輕量級程序(Light Weight Process ,LWP),輕量級程序就是我們通常意義上講的執行緒。輕量級程序和核心執行緒之間1:1的關係稱為一對一執行緒模型。

侷限性:
基於核心實現,各種執行緒操作需要進行系統呼叫,而系統呼叫的代價相對較高,需要在使用者態(User Model)和核心態(Kernel Model)中來回切換。
每個輕量級程序都需要一個核心執行緒支援,輕量級程序要消耗一定的核心資源,因此一個系統支援輕量級程序的數量是有限的。
使用使用者執行緒實現:狹義的使用者執行緒指的是完全建立在使用者空間的執行緒庫,系統核心不能感知執行緒存在。程序與執行緒之間的1:N的關係稱為一對多的執行緒模型。優勢在於不需要核心支援,劣勢也在於沒有核心支援,所以的執行緒操作都需要使用者程式自己處理。目前使用使用者執行緒的程式越來越少。

使用使用者執行緒加輕量級程序混合實現:使用者執行緒完全建立在使用者空間,使用者執行緒的建立、切換、析構的操作比較廉價,並且支援大規模的使用者執行緒併發。作業系統提供支援的輕量級程序則作為使用者執行緒和核心執行緒的橋樑使用核心提供的執行緒排程功能及處理器對映,並且使用者執行緒的系統呼叫要通過輕量級執行緒來完成,大大降低了整個程序被完全阻塞的風險。使用者執行緒與輕量級程序的數量比是不定的,即為N:M的關係。

執行緒的排程
執行緒排程是指系統為執行緒分配處理器使用權的過程,主要包括兩種方式。
協同式執行緒排程:執行時間由執行緒本身決定,執行完後,要主動通知系統切換到另一個執行緒。好處是實現簡單;切換操作對執行緒自己可知,所以沒有什麼同步問題;壞處是執行時間不可控,可能會出現程式一直阻塞的情況。
搶佔式執行緒排程:執行緒由系統分配執行時間,切換不由執行緒本身決定。執行時間系統可控,不會有一個執行緒導致整個程序阻塞的情況。Java使用的執行緒排程方式是搶佔式執行緒排程