Java線程圖文總結
實現方式
簡單介紹一下Java多線程實現方式,有以下三種:
1、繼承Thread類
2、實現Runnable接口
3、使用ExecutorService、Callable、Future實現有返回結果的多線程
區別是前兩種執行完之后不帶返回值,最后一種帶返回值,其中最常用為前兩種。
線程的狀態
java線程的整個生命周期有5個狀態:新建,就緒,運行中,阻塞,結束。
5個狀態之間的關系將結合下圖理解:
上圖為java線程生命周期期間的各種命運,下面介紹常見的幾種命運。
命運一:
新線程創建成功,調用start()進入就緒狀態,即進入待運行的線程池中等待,等待獲取CPU的使用權。當獲得CPU使用權,該線程從就緒狀態進入運行狀態。運行過程中,運氣好的,一次運行就把所要執行的任務執行完畢,線程結束;命運不好的,運行中途被CPU暫停運行,重新回到就緒狀態,等待分配,然后再等待進入運行期,直到最后運行完畢,最后結束。
命運二:
新線程創建成功,進入就緒狀態,獲取了CPU使用權,處于運行狀態。這里意外出現,該線程執行了sleep、yield、join三者其中一個命令。sleep、join需要被暫停執行一段時間,線程進入阻塞狀態。休息時間到,再重新進入就緒狀態;而yield是從運行狀態直接跳會就緒狀態。當到了就緒狀態后再重新等待CPU調度,重新進入運行期。run -> block -> run -> block.... 此種狀態會持續,一直到該線程的任務執行完畢。sleep、yield、join三者區別如下:
sleep:CPU暫停當前線程運行,同時讓就緒狀態(待運行池中)優先級較低的一個線程運行。當前線程被暫停后,會處于阻塞狀態n秒(n由開發人員設定),時間到了之后,會自動回到就緒狀態,等待CPU重新調度,重新從剛剛暫停的地方運行。注意,若代碼塊中包含了對象的鎖,在睡眠的過程中是不會釋放掉對象的鎖的,其他線程是不能訪問到共享數據的。
join:將指定的線程執行完成后,再運行當前線程剩下的任務。典型的例子是將兩個交替執行的線程合并順序執行。請結合下面代碼理解:
public static void main(String[] args) throws IOException { final Thread aThread = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 5; i++) { System.out.println("a:" + i); } } }); Thread bThread = new Thread(new Runnable() { @Override public void run() { try { aThread.join(); } catch (InterruptedException e) { } for (int i = 0; i < 5; i++) { System.out.println("b:" + i); } } }); bThread.start(); aThread.start(); try { bThread.join(); } catch (InterruptedException e) { } for (int i = 0; i < 5; i++) { System.out.println("c:" + i); } }
輸出結果:
a:0 a:1 a:2 a:3 a:4 b:0 b:1 b:2 b:3 b:4 c:0 c:1 c:2 c:3 c:4
如果把代碼中的join部分去掉,就不能保證a、b、c的輸出順序。
yield:實質是讓當前正在運行的線程直接回到就緒狀態,而不用進入阻塞狀態等待,且只會選擇優先級相同的線程進入運行狀態。若待運行的線程池中,沒有跟當前線程優先級相同的線程,或者當前線程又被選中運行,則當前線程還是會繼續運行,會誤造成yield無法達到目的的效果。
命運三:
線程a成功進入創建、就緒、運行流程,運行過程中,需要訪問資源i,而此時資源i正在被另外的線程b訪問并且上鎖了,此時線程a就會暫停運行,進入資源i的鎖池,等待線程b釋放資源i的鎖。當線程a獲得資源i的鎖時,會從資源i的鎖池中進入就緒狀態(待運行池),等待調度。以下為示例代碼:
public static void main(String[] args) throws IOException { final Object obj = 1; Thread aThread = new Thread(new Runnable() { @Override public void run() { synchronized(obj){ for (int i = 0; i < 3; i++) { System.out.println("線程a在使用obj..."); try { Thread.sleep(500); } catch (InterruptedException e) { } } } } }); Thread bThread = new Thread(new Runnable() { @Override public void run() { synchronized(obj){ for (int i = 0; i < 3; i++) { System.out.println("線程b在使用obj..."); try { Thread.sleep(500); } catch (InterruptedException e) { } } } } }); bThread.start(); aThread.start(); }
輸出結果是:
線程b在使用obj... 線程b在使用obj... 線程b在使用obj... 線程a在使用obj... 線程a在使用obj... 線程a在使用obj...
兩個線程都start()之后,兩個線程隨機先后訪問到obj對象,有可能是a先訪問obj,有可能是b先訪問obj。我的結果就是b先訪問obj,拿到obj的鎖,之后線程a無法立即訪問到obj,a就進入obj的鎖池中等待;當b中被鎖的代碼塊跑完且釋放鎖,a拿到obj的鎖,重新進入就緒狀態,等待分配運行。
命運四:
線程a成功的創建、就緒、運行,運行過程中調用obj.wait(),a就從run狀態進入到obj的等待池,等待其他線程調用obj.notify;當其他線程調用notify之后,線程a從obj的等待池中,進入到obj的鎖池,等待獲得鎖以重新進入就緒狀態恢復運行。下面具體解析一下wait、notify、notifyAll的作用:
wait:在已經獲得obj的鎖的前提下,主動釋放obj的鎖,釋放后當前線程會進入obj的等待池,即開始休眠。另外,當調用了wait之后,并不是立即釋放對象obj的鎖,而是在相應的synchronized()語句塊執行結束后才真正釋放。
notify的作用是從對象obj的等待池中隨機抽取一個線程放到對象obj的鎖池中,讓該線程繼續在鎖池中等待鎖,成功拿到鎖后,等待分配執行。
notifyAll的作用跟notify類似,只不過是把對象obj的等待池中的所有的線程全部取出,放到對象obj的鎖池中。
需要明確的一點:wait、notify、notifyAll均為在當前線程獲取對象的鎖的前提下執行的,所以這三個操作都必須在synchronized()塊中執行。
舉個例子:線程a、線程b、線程c需要協同工作,線程b、c的任務需要在a線程的任務完成后才能開始。那就是說a在完成任務后,需要通知b和c恢復工作。這種情況就可通過wait,notifyAll來實現協同工作。請看以下代碼:
public static void main(String[] args) { final Object object = new Object(); Thread a = new Thread() { public void run() { System.out.println("a start"); try { Thread.sleep(2000); } catch (InterruptedException e) { } synchronized (object) { System.out.println("a finish, notify b and c"); object.notifyAll(); //wait,notify,notifyAll必須在synchronized塊里面使用 } } }; Thread b = new Thread() { public void run() { synchronized (object) { System.out.println("b is starting, waiting for a finish..."); try { object.wait(); } catch (InterruptedException e) { } System.out.println("b end"); } } }; Thread c = new Thread() { public void run() { synchronized (object) { System.out.println("c is starting, waiting for a finish..."); try { object.wait(); } catch (InterruptedException e) { } System.out.println("c end"); } } }; a.start(); b.start(); c.start(); }
其中一次的輸出結果:
a start b is starting, waiting for a finish... c is starting, waiting for a finish... a finish, notify b and c c end b end
因為線程在鎖池中是被隨機抽取的,所以不可能保證b,c哪個先運行。上面的結果就是c先被抽取。
上述全部討論的前提是線程運行中沒有遇到exception的情況,若遇上了exception,就直接end。
以上為本人對Java線程的理解,是基于線程的淺層部分展開討論,歡迎指正。若想深入了解線程,可以看看java.util.concurrent包下的類。本人之前寫過一個基于多線程實現的遠程監控程序,有興趣可參考一下:
http://my.oschina.net/ericquan8/blog/383882