如果要問我Java當中最難的部分是什麼?最有意思的部分是什麼?最多人討論的部分是什麼?那我會毫不猶豫地說:多線程。
Java多線程說它難,也不難,就是有點繞;說它簡單,也不簡單,需要理解的概念很多,尤其是很多底層知識,如數據結構、操作系統的部分。
Java多線程掌握得好,不僅僅只是對Java,對任何其他具有併發特性的編程語言,甚至是操作系統,都能有更全面和準確的認識。
Java多線程最大的特點,而且也是唯一確定的一件事,那就是:在多線程環境下,程序的運行結果是無法預料的,但這也正是它最有趣的地方。
在瞭解多線程之前,最好先知道什麼是併發,什麼是並行。不然很容易迷糊。
總的來說,就是這樣:
並行:同一時刻可以同時發生/執行多個任務。
併發:同一時刻只能發生/執行一個任務。
學習多線程最好從如下六個方面循序漸進(純粹個人經驗和建議,可無視):
1、線程生命週期:NEW、RUNNABLE(READY、RUNNING)、BLOCKED、WAITING、TIMED_WAITING、TERMINATED狀態
2、關鍵字:synchronized和volatile
3、線程池:ThreadPoolExecutor
4、鎖(AQS):悲觀鎖/樂觀鎖、輕量級鎖/重量級鎖、自旋鎖/可重入鎖等各種鎖
5、CAS:各種原子類
6、併發工具類:ArrayBlockingQueue、CountDownLatch、CyclicBarrier、Semaphore等
Java多線程用一句話總結就是「6類5法」。
所謂「6類」,就是多狀態的狀態分爲這6類:
1、新建(NEW):新創建了一個線程,但還沒調用start方法
2、運行(RUNNABLE)
2.1、就緒(ready):運行start方法後,線程位於可運行線程池中,等待被調度
2.2、運行中(RUNNING):就緒的線程獲得CPU的時間片就變爲運行中
3、阻塞(BLOCKED):線程等待獲取鎖
4、等待(WAITING):接收事件通知後或系統中斷後進入等待
5、超時(TIMED_WAITING):等待指定時間後會自行返回
6、終止(TERMINATED):線程已執行完畢
這是線程生命週期的狀態變化圖:
簡單來說,就是這樣:
而所謂「5法」就是線程的核心方法是這麼5個:
1、wait:當前線程調用鎖對象的wait方法,當前線程釋放鎖,進入等待狀態,由其他線程接着執行
2、notify/notifyAll:喚醒任意一個或全部等待的線程後接着執行,但並不釋放鎖
3、join:當前線程調用其他線程的join方法,調用後當前線程進入等待狀態
4、yield:當前線程調用,調用後暫停執行(可能無效),變爲就緒態
5、sleep:當前線程調用,調用後進入TIME_WAITING狀態
用代碼來解釋一下會更直觀一些。
第一種wait/notify的情況:
public static void main(String[] args) { Thread t1 = new Thread(new Runnable() { @Override public void run() { synchronized ("鎖") { System.out.println("t1 start"); try { // t1釋放鎖 "鎖".wait(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("t1 end"); } } }); Thread t2 = new Thread(new Runnable() { @Override public void run() { synchronized ("鎖") { System.out.println("t2 start"); try { // 通知t1進入等待隊列 "鎖".notify(); } catch (Exception e) { e.printStackTrace(); } System.out.println("t2 end"); } } }); t1.start(); t2.start(); }
此時代碼執行流程(兩種可能):
1、T1先執行
1.1、T1啓動,wait讓出鎖,讓出CPU,T2獲得CPU,T2啓動,notify了通過object鎖等待的線程
1.2、T1被喚醒後等待啓動,T2繼續執行,T2執行完,T1獲得CPU後繼續執行
2、T2先執行
2.1、T2執行完,T1啓動,讓出CPU,由於沒有線程再來執行notify,程序無限期等待
這裏要強調的重點是:
1、wait會讓出CPU而notify不會
2、wait重點在通知其它同用一個object的線程“我暫時不用了”,並且讓出CPU
3、notify重點在於通知使用object的對象“我用完了!”
如果說只有兩個線程的時候,還能嘗試着分析一下結果,那麼當有四個線程的時候會如何呢?看看代碼:
public static void main(String[] args) { Thread t1 = new Thread(new Runnable() { @Override public void run() { synchronized ("鎖") { System.out.println("t1 start"); try { // t1釋放鎖 "鎖".wait(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("t1 end"); } } }); Thread t2 = new Thread(new Runnable() { @Override public void run() { synchronized ("鎖") { try { System.out.println("t2 start"); // 隨機通知一個等待的線程進入等待隊列 "鎖".notify(); System.out.println("t2 end"); } catch (Exception e) { e.printStackTrace(); } } } }); Thread t3 = new Thread(new Runnable() { @Override public void run() { synchronized ("鎖") { try { System.out.println("t3 start"); // 隨機通知一個等待的線程進入等待隊列 "鎖".notify(); System.out.println("t3 end"); } catch (Exception e) { e.printStackTrace(); } } } }); Thread t4 = new Thread(new Runnable() { @Override public void run() { synchronized ("鎖") { try { System.out.println("t4 start"); // t4釋放鎖 "鎖".wait(); System.out.println("t4 end"); } catch (Exception e) { e.printStackTrace(); } } } }); t1.start(); t2.start(); t3.start(); t4.start(); }
然後同時開啓這四個線程,但結果是無法預料!爲什麼?因爲只有兩種可能的流程(要麼wait先執行完,要麼notify先執行完),至於每種流程裏面怎麼執行的?不知道!不清楚!無法預料!這就是多線程讓人困惑的地方和魅力所在。
而且線程還有一個無賴的行爲就是:雖然你有優先級,但我不保證有用!
public class MyThread extends Thread { MyThread(String s) { super(s); } @Override public void run() { for (int i = 0; i <= 10; i++) { System.out.println(getName() + " : " + i); if (i == 5) { Thread.yield(); } } } public static void main(String[] args) throws InterruptedException { System.out.println("主線程啓動"); Thread t1 = new MyThread("t1"); Thread t2 = new MyThread("t2"); t1.setPriority(Thread.MIN_PRIORITY); t1.start(); t2.setPriority(Thread.MAX_PRIORITY); t2.start(); t1.join(); t2.join(); System.out.println("主線程結束"); } }
這裏不管怎麼設置t1或者t2的優先級,都沒有用,運行的結果每次都可能不一樣。
線程的生命週期6類5法算是比較簡單的,是基礎中的基礎。但是用好很難,關鍵在於多練多想,多多嘗試各種組合。