java多線程我個人覺得是javaSe中最難的一部分,我以前也是感覺學會了,但是真正有多線程的需求卻不知道怎麼下手,實際上還是對多線程這塊知識了解不深刻,不知道多線程api的應用場景,不知道多線程的運行流程等等,本篇文章將使用實例+圖解+源碼的方式來解析java多線程。
文章篇幅較長,大家也可以有選擇的看具體章節,建議多線程的代碼全部手敲,永遠不要相信你看到的結論,自己編碼後運行出來的,才是自己的。
進程
線程
並發:單核cpu運行多線程時,時間片進行很快的切換。線程輪流執行cpu
並行:多核cpu運行 多線程時,真正的在同一時刻運行
java提供了豐富的api來支持多線程。
多線程能實現的都可以用單線程來完成,那單線程運行的好好的,為什麼java要引入多線程的概念呢?
多線程的好處:
單線程只有一條執行線,過程容易理解,可以在大腦中清晰的勾勒出代碼的執行流程
多線程卻是多條線,而且一般多條線之間有交互,多條線之間需要通信,一般難點有以下幾點
有時候希望自己變成一個字節穿梭於伺服器中,搞清楚來龍去脈,就像無敵破壞王一樣(沒看過這部電影的可以看下,腦洞大開)。
任務:線程的執行體。也就是我們的核心代碼邏輯
定義任務
Thread實現任務的局限性
Runnable和Callable解決了Thread的局限性
但是Runbale相比Callable有以下的局限性
如下代碼 幾種定義線程的方式
@Slf4jclass T extends Thread { @Override public void run() { log.info(&34;); }}@Slf4jclass R implements Runnable { @Override public void run() { log.info(&34;); }}@Slf4jclass C implements Callable<String> { @Override public String call() throws Exception { log.info(&34;); return &34;; }}
創建線程的方式
啟動線程的方式
// 啟動繼承Thread類的任務new T().start();// 啟動繼承Thread匿名內部類的任務 可用lambda優化Thread t = new Thread(){ @Override public void run() { log.info(&34;); }};// 啟動實現Runnable接口的任務new Thread(new R()).start();// 啟動實現Runnable匿名實現類的任務new Thread(new Runnable() { @Override public void run() { log.info(&34;); }}).start();// 啟動實現Runnable的lambda簡化後的任務new Thread(() -> log.info(&34;)).start();// 啟動實現了Callable接口的任務 結合FutureTask 可以獲取線程執行的結果FutureTask<String> target = new FutureTask<>(new C());new Thread(target).start();log.info(target.get());
以上各個線程相關的類的類圖如下
多核cpu下,多線程是並行工作的,如果線程數多,單個核又會並發的調度線程,運行時會有上下文切換的概念
cpu執行線程的任務時,會為線程分配時間片,以下幾種情況會發生上下文切換。
當發生上下文切換時,作業系統會保存當前線程的狀態,並恢復另一個線程的狀態,jvm中有塊內存地址叫程序計數器,用於記錄線程執行到哪一行代碼,是線程私有的。
idea打斷點的時候可以設置為Thread模式,idea的debug模式可以看出棧幀的變化
yield()方法會讓運行中的線程切換到就緒狀態,重新爭搶cpu的時間片,爭搶時是否獲取到時間片看cpu的分配。
代碼如下
// 方法的定義public static native void yield();Runnable r1 = () -> { int count = 0; for (;;){ log.info(&34; + count++); }};Runnable r2 = () -> { int count = 0; for (;;){ Thread.yield(); log.info(&34; + count++); }};Thread t1 = new Thread(r1,&34;);Thread t2 = new Thread(r2,&34;);t1.start();t2.start();// 運行結果11:49:15.796 [t1] INFO thread.TestYield - ---- 1>12950411:49:15.796 [t1] INFO thread.TestYield - ---- 1>12950511:49:15.796 [t1] INFO thread.TestYield - ---- 1>12950611:49:15.796 [t1] INFO thread.TestYield - ---- 1>12950711:49:15.796 [t1] INFO thread.TestYield - ---- 1>12950811:49:15.796 [t1] INFO thread.TestYield - ---- 1>12950911:49:15.796 [t1] INFO thread.TestYield - ---- 1>12951011:49:15.796 [t1] INFO thread.TestYield - ---- 1>12951111:49:15.796 [t1] INFO thread.TestYield - ---- 1>12951211:49:15.798 [t2] INFO thread.TestYield - ---- 2>29311:49:15.798 [t1] INFO thread.TestYield - ---- 1>12951311:49:15.798 [t1] INFO thread.TestYield - ---- 1>12951411:49:15.798 [t1] INFO thread.TestYield - ---- 1>12951511:49:15.798 [t1] INFO thread.TestYield - ---- 1>12951611:49:15.798 [t1] INFO thread.TestYield - ---- 1>12951711:49:15.798 [t1] INFO thread.TestYield - ---- 1>129518
如上述結果所示,t2線程每次執行時進行了yield(),線程1執行的機會明顯比線程2要多。
線程的優先級
線程內部用1~10的數來調整線程的優先級,默認的線程優先級為NORM_PRIORITY:5
cpu比較忙時,優先級高的線程獲取更多的時間片
cpu比較閒時,優先級設置基本沒用
public final static int MIN_PRIORITY = 1; public final static int NORM_PRIORITY = 5; public final static int MAX_PRIORITY = 10; // 方法的定義 public final void setPriority(int newPriority) { }
cpu比較忙時
Runnable r1 = () -> { int count = 0; for (;;){ log.info(&34; + count++); }};Runnable r2 = () -> { int count = 0; for (;;){ log.info(&34; + count++); }};Thread t1 = new Thread(r1,&34;);Thread t2 = new Thread(r2,&34;);t1.setPriority(Thread.NORM_PRIORITY);t2.setPriority(Thread.MAX_PRIORITY);t1.start();t2.start();// 可能的運行結果11:59:00.696 [t1] INFO thread.TestYieldPriority - ---- 1>4410211:59:00.696 [t2] INFO thread.TestYieldPriority - ---- 2>13590311:59:00.696 [t2] INFO thread.TestYieldPriority - ---- 2>13590411:59:00.696 [t2] INFO thread.TestYieldPriority - ---- 2>13590511:59:00.696 [t2] INFO thread.TestYieldPriority - ---- 2>135906
cpu比較閒時
Runnable r1 = () -> { int count = 0; for (int i = 0; i < 10; i++) { log.info(&34; + count++); }};Runnable r2 = () -> { int count = 0; for (int i = 0; i < 10; i++) { log.info(&34; + count++); }};Thread t1 = new Thread(r1,&34;);Thread t2 = new Thread(r2,&34;);t1.setPriority(Thread.MIN_PRIORITY);t2.setPriority(Thread.MAX_PRIORITY);t1.start();t2.start();// 可能的運行結果 線程1優先級低 卻先運行完12:01:09.916 [t1] INFO thread.TestYieldPriority - ---- 1>712:01:09.916 [t1] INFO thread.TestYieldPriority - ---- 1>812:01:09.916 [t1] INFO thread.TestYieldPriority - ---- 1>912:01:09.916 [t2] INFO thread.TestYieldPriority - ---- 2>212:01:09.916 [t2] INFO thread.TestYieldPriority - ---- 2>312:01:09.916 [t2] INFO thread.TestYieldPriority - ---- 2>412:01:09.916 [t2] INFO thread.TestYieldPriority - ---- 2>512:01:09.916 [t2] INFO thread.TestYieldPriority - ---- 2>612:01:09.916 [t2] INFO thread.TestYieldPriority - ---- 2>712:01:09.916 [t2] INFO thread.TestYieldPriority - ---- 2>812:01:09.916 [t2] INFO thread.TestYieldPriority - ---- 2>9
默認情況下,java進程需要等待所有線程都運行結束,才會結束,有一種特殊線程叫守護線程,當所有的非守護線程都結束後,即使它沒有執行完,也會強制結束。
默認的線程都是非守護線程。
垃圾回收線程就是典型的守護線程
// 方法的定義public final void setDaemon(boolean on) {}Thread thread = new Thread(() -> { while (true) { }});// 具體的api。設為true表示未守護線程,當主線程結束後,守護線程也結束。// 默認是false,當主線程結束後,thread繼續運行,程序不停止thread.setDaemon(true);thread.start();log.info(&34;);
線程的阻塞可以分為好多種,從作業系統層面和java層面阻塞的定義可能不同,但是廣義上使得線程阻塞的方式有下面幾種
使線程休眠,會將運行中的線程進入阻塞狀態。當休眠時間結束後,重新爭搶cpu的時間片繼續運行
// 方法的定義 native方法public static native void sleep(long millis) throws InterruptedException; try { // 休眠2秒 // 該方法會拋出 InterruptedException異常 即休眠過程中可被中斷,被中斷後拋出異常 Thread.sleep(2000); } catch (InterruptedException異常 e) { } try { // 使用TimeUnit的api可替代 Thread.sleep TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { }
join是指調用該方法的線程進入阻塞狀態,等待某線程執行完成後恢復運行
// 方法的定義 有重載// 等待線程執行完才恢復運行public final void join() throws InterruptedException {}// 指定join的時間。指定時間內 線程還未執行完 調用方線程不繼續等待就恢復運行public final synchronized void join(long millis) throws InterruptedException{}
Thread t = new Thread(() -> { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } r = 10;});t.start();// 讓主線程阻塞 等待t線程執行完才繼續執行 // 去除該行,執行結果為0,加上該行 執行結果為10t.join();log.info(&34;, r);// 運行結果13:09:13.892 [main] INFO thread.TestJoin - r:10
// 相關方法的定義public void interrupt() {}public boolean isInterrupted() {}public static boolean interrupted() {}
打斷標記:線程是否被打斷,true表示被打斷了,false表示沒有
isInterrupted() 獲取線程的打斷標記 ,調用後不會修改線程的打斷標記
interrupt()方法用於中斷線程
interrupted() 獲取線程的打斷標記,調用後清空打斷標記 即如果獲取為true 調用後打斷標記為false (不常用)
interrupt實例:有個後臺監控線程不停的監控,當外界打斷它時,就結束運行。代碼如下
@Slf4jclass TwoPhaseTerminal{ // 監控線程 private Thread monitor; public void start(){ monitor = new Thread(() ->{ // 不停的監控 while (true){ Thread thread = Thread.currentThread(); // 判斷當前線程是否被打斷 if (thread.isInterrupted()){ log.info(&34;); break; } try { Thread.sleep(1000); // 監控邏輯中被打斷後,打斷標記為true log.info(&34;); } catch (InterruptedException e) { // 睡眠時被打斷時拋出異常 在該處捕獲到 此時打斷標記還是false // 在調用一次中斷 使得中斷標記為true thread.interrupt(); } } }); monitor.start(); } public void stop(){ monitor.interrupt(); }}
上面說了一些基本的api的使用,調用上面的方法後都會使得線程有對應的狀態。
線程的狀態可從 作業系統層面分為五種狀態 從java api層面分為六種狀態。
Thread類中的內部枚舉State
public enum State { NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED;}
六種線程狀態和方法的對應關係
主要總結Thread類中的核心方法
方法名稱是否static方法說明start()否讓線程啟動,進入就緒狀態,等待cpu分配時間片run()否重寫Runnable接口的方法,線程獲取到cpu時間片時執行的具體邏輯yield()是線程的禮讓,使得獲取到cpu時間片的線程進入就緒狀態,重新爭搶時間片sleep(time)是線程休眠固定時間,進入阻塞狀態,休眠時間完成後重新爭搶時間片,休眠可被打斷join()/join(time)否調用線程對象的join方法,調用者線程進入阻塞,等待線程對象執行完或者到達指定時間才恢復,重新爭搶時間片isInterrupted()否獲取線程的打斷標記,true:被打斷,false:沒有被打斷。調用後不會修改打斷標記interrupt()否打斷線程,拋出InterruptedException異常的方法均可被打斷,但是打斷後不會修改打斷標記,正常執行的線程被打斷後會修改打斷標記interrupted()否獲取線程的打斷標記。調用後會清空打斷標記stop()否停止線程運行 不推薦suspend()否掛起線程 不推薦resume()否恢復線程運行 不推薦currentThread()是獲取當前線程
Object中與線程相關方法
方法名稱方法說明wait()/wait(long timeout)獲取到鎖的線程進入阻塞狀態notify()隨機喚醒被wait()的一個線程notifyAll();喚醒被wait()的所有線程,重新爭搶時間片