1.????前言~🥳🎉🎉🎉
Hello, Hello~ 親愛的朋友們👋👋,這里是E綿綿呀????。
如果你喜歡這篇文章,請別吝嗇你的點贊????和收藏📖📖。如果你對我的內容感興趣,記得關注我👀👀以便不錯過每一篇精彩。
當然,如果在閱讀中發現任何問題或疑問,我非常歡迎你在評論區留言指正🗨?🗨?。讓我們共同努力,一起進步!
加油,一起CHIN UP!💪💪
🔗個人主頁:E綿綿的博客
📚所屬專欄:1.?JAVA知識點專欄
? ? ?? ?深入探索JAVA的核心概念與技術細節
2.JAVA題目練習
? ? ? ??實戰演練,鞏固JAVA編程技能
3.c語言知識點專欄
? ? ? ? 揭示c語言的底層邏輯與高級特性
4.c語言題目練習
? ? ? ? 挑戰自我,提升c語言編程能力
5.Mysql數據庫專欄
? ? ? ? 了解Mysql知識點,提升數據庫管理能力
6.html5知識點專欄
? ? ? ? 學習前端知識,更好的運用它
7.?css3知識點專欄
? ? ? ? 在學習html5的基礎上更加熟練運用前端
8.JavaScript專欄
? ? ? ? 在學習html5和css3的基礎上使我們的前端使用更高級、
9.JavaEE專欄
? ? ? ? 學習更高階的Java知識,讓你做出網站
📘 持續更新中,敬請期待????
2.認識線程?
線程是什么
一個線程就是一個?"?執行流?".?每個線程之間都可以按照順序執行自己的代碼?.?多個線程之間?"?同時?"?執行著多份代碼。
我們設想如下場景:
? ? ? ?一家公司要去銀行辦理業務,既要進行財務轉賬,又要進行福利發放,還得進行繳社保。如果只有張三一個會計就會忙不過來,耗費的時間特別長。為了讓業務更快的辦理好,張三又找來兩位同事李四、王五一起來幫助他,三個人分別負責一個事情,分別申請一個號碼進行排隊,自此就有了三個執行流共同完成任務,但本質上他們都是為了辦理一家公司的業務。此時,我們就把這種情況稱為多線程,將一個大任務分解成不同小任務,交給不同執行流就分別排隊執行。其中李四、王五都是張三叫來的,所以張三一般被稱為主線程(Main Thread)。
為什么要有線程?
首先, "并發編程" 成為 剛需。
- 單核?CPU?的發展遇到了瓶頸.?要想提高算力,?就需要多核?CPU,而并發編程能更充分利用多核?CPU資源。
- 有些任務場景需要?"等待?IO",?為了讓等待?IO?的時間能夠去做一些其他的工作,?也需要用到并發編程。
其次, 雖然多進程也能實現并發編程, 但是線程比進程更輕量。線程是輕量級進程。
- 創建線程比創建進程更快。
- 銷毀線程比銷毀進程更快。
- 調度線程比調度進程更快。
最后, 線程雖然比進程輕量, 但是人們還不滿足, 于是又有了 "線程池"和 "協程",關于線程池和協程我后面會再介紹。
進程和線程的區別
之前講過一個進程一般使用一個或者多個 PCB 來表示,之所以可以由多個PCB表示,是因為其實一個線程才是由一個PCB來表示,進程中有一個或者多個線程。?
對于線程,它們的PCB屬性跟上篇文章說的一樣,也是主要有這七個屬性,那么對于一個進程中的線程,它們的屬性是相同的嗎?
答案是 前三個都是一樣的,后面四個不一樣。
從而可以得出同一個進程中的若干線程之間,是共用相同的內存資源和文件資源的,線程1 中 new 個對象,線程2 是可以訪問到的; 線程1 打開一個文件,線程2 也是可以直接使用的,這樣每個線程就不用單獨申請內存,可以共用,效率就更高。但是每個線程都是獨立的在 cpu 上調度執行.
so可以得出以下結論:?
- 進程是包含線程的,每個進程至少有一個線程存在。
- 進程和進程之間不共享內存空間,同一個進程的線程之間共享同一個內存空間。
- 進程是系統分配資源的最小單位,線程是系統調度的最小單位。?
Java?的線程和操作系統線程的關系
線程是操作系統中的概念。?操作系統內核實現了線程這樣的機制?,?并且對用戶層提供了一些?API?供用戶使用。
Java?標準庫中?Thread?類可以視為是對操作系統提供的?API?進行了進一步的抽象和封裝。從而達到跨系統(可以用于多個系統)
java中的主線程?
對于java中自帶一個主線程,main就是java的主線程,在其里面的代碼都是主線程中的任務
jconsole觀察線程
我們可以通過jconsole這個工具去觀察線程,至于怎么使用,這里有一篇文章寫的很好:
觀測線程的工具——jconsole_觀測電腦線程-CSDN博客
3.創建線程?
?創建線程有兩種方法:
?方法1——繼承Thread類
通過繼承?
Thread
?類并重寫?run()
?方法來創建線程。class MyThread extends Thread {@Overridepublic void run() {System.out.println("Thread is running (Thread Class)");System.out.println("Thread name: " + Thread.currentThread().getName());} }public class Main {public static void main(String[] args) {MyThread thread = new MyThread();thread.start(); // 啟動線程} }
方法2——實現Runnable接口?
通過實現?
Runnable
?接口來定義線程任務。class MyRunnable implements Runnable {@Overridepublic void run() {System.out.println("Thread is running (Runnable)");System.out.println("Thread name: " + Thread.currentThread().getName());} }public class Main {public static void main(String[] args) {Thread thread = new Thread(new MyRunnable());thread.start(); // 啟動線程} }
?在講完這兩個方法后,我們發現run和start兩個重要方法,那么它們是干什么的?
run里面用于描述線程干什么任務,通過 start 去創建線程而后執行該任務。
start是Thread中自帶的方法,通俗的來說run方法記錄這個"事情",而strat就是要執行run里面的"事情"
如果不去調用start,直接調用run的話,就沒有創建出新的線程,就是在主線程中執行run任務。
使用匿名內部類創建線程
其實對于上述代碼,我們可以用匿名內部類讓其更簡便。
public class Main {public static void main(String[] args) {// 使用匿名內部類繼承 ThreadThread thread = new Thread() {@Overridepublic void run() {System.out.println("Thread is running (Thread - Anonymous Inner Class)");System.out.println("Thread name: " + Thread.currentThread().getName());}};// 啟動線程thread.start();}
}
public class Main {public static void main(String[] args) {// 使用匿名內部類實現 RunnableRunnable runnable = new Runnable() {@Overridepublic void run() {System.out.println("Thread is running (Runnable - Anonymous Inner Class)");System.out.println("Thread name: " + Thread.currentThread().getName());}};// 創建線程并啟動Thread thread = new Thread(runnable);thread.start();}
}
? 使用Lambda 表達式創建線程
?Lambda 表達式 我們之前學過,可以將匿名內部類更加簡潔出現,但只適用于函數式接口,這里只可以用于簡化實現runna接口的方法。
public class Main {public static void main(String[] args) {// 使用 Lambda 表達式實現 RunnableRunnable runnable = () -> {System.out.println("Thread is running (Runnable - Lambda Expression)");System.out.println("Thread name: " + Thread.currentThread().getName());};// 創建線程并啟動Thread thread = new Thread(runnable);thread.start();}
}
?由于?
Thread
?類本身不是函數式接口,所以無法直接使用 Lambda 表達式
對于匿名內部類和Lambda 表達式如果有忘的同學可以看看這篇文章
【Java數據結構】反射、枚舉以及lambda表達式_java反射獲取lamda表達式-CSDN博客
4.Thread類及常見方法
Thread類的概念?
我們在前面看到了線程的創建需要Thread類,那么Thread類到底是什么呢?
Thread 類是 JVM 用來管理線程的一個類,換句話說,每個線程都有一個唯一的 Thread 對象與之關聯。
每個執行流,也需要有一個對象來描述,而 Thread 類的對象就是用來描述一個線程執行流的,JVM 會將這些 Thread 對象組織起來,用于線程調度,線程管理,
Thread 的常見構造方法?
這些剛才都學過,就不繼續說了?
Thread 的幾個常見屬性?
常見屬性說明:
ID 是線程的唯一標識,不同線程不會重復,這里的id和pcb的id是不同的,是jvm自己搞的一套體系,Java代碼也無法獲取到pcb的id
名稱是各種調試工具用到
狀態表示線程當前所處的一個情況,這里有六種狀態
1.NEW(新建)
線程對象被創建,但尚未啟動。此時線程還未開始執行。2.TERMINATED(終止)
線程已經完成了執行,線程對象還存在。3.RUNNABLE(可運行)
線程已經啟動,正在執行。4.TIMED WAITING(計時等待)
線程進入一個有時間限制的等待狀態。可以通過Thread.sleep(long millis)、Obiect.wait(long timeout)Thread.join(long millis)進行。
5.WAITING(等待)
線程進入等待狀態直到其他線程顯式地喚醒它。可以通過Obiect.wait()、Thread.join()方法進入此狀態
6.BLOCKED(阻塞)
線程因為等待獲取一個監視器鎖而進入阻塞狀態。通常發生在同步代碼塊或方法中,線程試圖獲取一個已經被其他線程持有的鎖。
對于后面三個狀態講sleep,join,wait以及鎖時,才能理解這三種狀態。現在看可能有點看不懂,待我在后面講解。
優先級高的線程理論上來說更容易被調度到
關于后臺線程,需要記住一點:JVM會在一個進程的所有前臺線程結束后,才會結束運行。而后臺線程無論結束還是不結束,都不影響jvm結束運行
在 java 代碼中, main 線程, 就是前臺線程另外程序員創建出來的線程,默認情況下都是 前臺線程.可以通過上述 setDaemon 方法來把線程設置為后臺線程
在 jconsole 中看到的 jvm 中包含一些其他的內置的線程, 就屬于后臺線程。我不期望這個線程影響JVM的結束,就設為后臺線程,舉個例子,比如,有的線程負責進行 gc. (垃圾回收),gc 是要周期性持續性執行的.不可能主動結束,要是把他設為前臺,進程就永遠也結束不了?
是否存活,很直白的意思,就是線程是否還存在
是否被中斷(isInterrupted)這個屬性之后在中斷會講到,這里先打個啞謎。
start()-啟動一個線程?
start 才是正在的創建線程(在內核中創建pcb),一個線程需要通過run/lambda把線程要執行的任務定義出來,start 才是正在的創建線程,并開始執行
一個 Thread 對象只能 start 一次
線程中斷?
在 Java 中,線程的中斷(Interrupt)是一種協作機制,用于通知線程應該停止當前的任務并退出。線程中斷并不是強制終止線程,而是通過設置線程的中斷狀態來通知線程,線程可以根據中斷狀態決定如何響應,是否要中斷。?
操作系統原生的線程中,其實是有辦法讓別的線程直接被強制終止的,這種設定其實不太好, 所以 java 沒有采納過來,
Thread 內部包含了一個 boolean 類型的變量作為線程是否被中斷的標記?,我們稱該變量為中斷狀態。
Java 提供了以下三個與線程中斷相關的方法:
方法名 說明 void interrupt()
設置中斷狀態為true。如果線程正在阻塞狀態(如? sleep,
wait,join
),會拋出?InterruptedException,并設置中斷狀態為false
boolean isInterrupted()
檢查目標線程的中斷狀態,不會清除中斷狀態。 static boolean interrupted()
檢查當前線程的中斷狀態,并清除中斷狀態。這個用的少。
下面是有關線程中斷的使用代碼,可以利用以上三個方法結合一些代碼 終止線程或者提前終止線程。
public class Main {public static void main(String[] args) {Thread thread = new Thread(() -> {while (!Thread.currentThread().isInterrupted()) {try {System.out.println("Thread is running...");Thread.sleep(1000); // 線程休眠 1 秒} catch (InterruptedException e) {System.out.println("Thread was interrupted while sleeping!");// 恢復中斷狀態Thread.currentThread().interrupt();}}System.out.println("Thread was interrupted!");});thread.start();// 主線程休眠 3 秒后中斷子線程try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}thread.interrupt(); // 中斷子線程} }
輸出:
Thread is running...
Thread is running...
Thread was interrupted while sleeping!
Thread was interrupted!
在捕獲?InterruptedException
?后,調用?Thread.currentThread().interrupt()
?恢復中斷狀態。這樣可以確保線程能夠正確退出。
在該代碼中線程陷入死循環,此時通過設計我們就可以通過interrupt去終止該死循環線程。
這里還是寫的最簡單的邏輯代碼,之后會出現更高級的邏輯代碼,更加復雜。
?等待一個線程——join()
?有時,我們需要等待一個線程完成它的工作后,才能進行自己的下一步工作。
由于線程是"搶占式"執行且并發執行,所以誰先結束每次都是不確定的,如果希望讓代碼里面的 t 先結束,main后結束,就可以在main中使用線程等待 t.join()。?
上述只是join無參數版本的,也就是死等,只要 t 不結束,就會一直等待下去,還要帶參數的版本。
在實際開發中,一般很少使用死等這個策略
傳入一個時間:傳入的時間是最大等待時間,比如寫的等待100s,如果100s之內,t 線程結束了之間返回,如果100s到了,t 線程還沒有結束不等了!!!繼續往下走。
?獲取當前線程引用
?這個方法我們之前用過,現在來看下吧。
它可以獲取當前代碼所在線程的對象。
休眠當前線程——sleep()?
它也是我們比較熟悉一組方法,有一點要記得,因為線程的調度是不可控的,所以,這個方法只能保證實際休眠時間是大于等于參數設置的休眠時間的。
?join()和sleep()的異常
join()
?和?sleep()
?方法都會拋出?InterruptedException
,這是因為它們都是可中斷的阻塞方法。當線程在調用這些方法時,如果被其他線程中斷(通過調用?interrupt()
?方法),就會拋出?InterruptedException
。為了確保程序的健壯性,我們需要在代碼中正確處理這個異常。通常有兩種方式來處理?
InterruptedException
:
使用?
throws
?聲明拋出異常。使用?
try-catch
?捕獲并處理異常。這樣就不會編譯錯誤,能正常運行,那么該用哪種方式處理呢?
我認為用第二種是最好的,因為假如沒異常,第一種第二種都能正常運行,而真出現異常了,第一種會運行錯誤,第二種能真正解決。
并且如果在重寫的run()方法中throws interruptedException? 就會發生編譯錯誤,由于父類不存在throws interruptedException?,重寫的方法就不能throws interruptedException ,只能try catch 解決異常。
所以在面對join()和sleep()時,我們都一致用try catch解決
InterruptedException
異常。后面還會學一個wait()方法,它也是跟join()和sleep()一樣,也是用try catch解決InterruptedException
異常。
5.線程安全?
線程安全的概念?
想給出一個線程安全的確切定義是復雜的,但我們可以這樣認為:?如果多線程環境下代碼運行的結果是符合我們預期的,即在單線程環境應該的結果,則說這個程序是線程安全的。
線程不安全案例?
?下面我們給出線程不安全的案例:
首先,我們寫一個多線程代碼,一個線程負責把一個數加50000次,另一個線程也負責把這個數加50000次。(從0開始加)
class Counter{private int count = 0;public void increase(){count++;}public int getCount() {return count;} } public class Test4 {public static void main(String[] args) throws InterruptedException {Counter counter = new Counter();Thread t1 = new Thread(()->{for (int i = 0; i < 50000; i++) {counter.increase();}});Thread t2 = new Thread(()->{for (int i = 0; i < 50000; i++) {counter.increase();}});t1.start();t2.start();t1.join();t2.join();System.out.println(counter.getCount());} }
按照我們的邏輯,希望最后得到的結果是100000,為什么會出現這么大的偏差呢?其實,這樣的運行結果是由于線程不安全導致的。
事實上:上面的count++操作,在CPU指令角度看,本質上是三個操作:
把內存中的數據加載到CPU的寄存器中。(LOAD)
把寄存器中的寄存器進行加一運行算。(ADD)
把寄存器中的數據寫回到內存中。(save)如果是單個線程執行,沒有問題。但是如果是多個線程并發執行,就可能會出現錯誤。由于CPU調度線程的順序是不確定的,因此這兩個線程并發執行的過程中,線程1的操作可能會和線程2的操作相互影響,也就是說,這兩個線程的命令的排列方式可能有很多種:
從而因為這樣就導致了線程不安全。那么怎么解決該問題呢?看下文可知:
解決線程不安全問題?
線程不安全問題的原因
線程不安全的原因有多個:?
1.多個線程之間的調度順序是隨機的,操作系統使用搶占式執行的策略來調度線程。【根本原因】
2.多個線程同時修改同一個變量,容易產生線程安全問題。一個線程修改同一個變量 =>沒事.
多個線程讀取同一個變量 =>沒事.每次讀到的結果都是固定的,
多個線程修改不同的變量 =>沒事.你改你的,我改我的,不影響.
3.進行的修改,不是原子性的。如果修改操作,能夠按照原子的方式來完成,就不會出現線程安全問題。?原子性是指在一個操作在cpu中不可以中途暫停然后再調度,即不被中斷操作,要不全部執行完成,要不都不執行(Mysql學過)
4.內存可見性引起的線程安全問題。
5.指令重排序,引起的線程安全問題。
要解決該案例我們首先得看是哪個原因造成的? 我們發現是前三個原因造成的。那么我們試著從這三個原因為出發點去解決它。
第一個原因,我們改變不了,因為內核已經是搞好了的,我們自己改也沒用
第二個原因通過調整代碼結構,盡量避免出現拿多個線程同時改同一個變量,這是一個切入點,但是在Java中,這種做法不是很普適,只是針對一些特定的場景是可以做到的。
第三個原因,這是解決線程安全問題最普適的方案
?該問題不是由內存可見性和指令重排序引起的,所以現在先不講,后面會講到。
解決方案——synchronized進行加鎖操作?
我們可以把修改操作改成“原子性”的操作,那么怎么操作呢?
可以進行加鎖操作。相當于是把一組操作,打包成一個整體的操作。此處這里的原子性,是通過鎖進行“互斥”,當前線程執行的時候,其他線程無法執行。對于加鎖,Java引入了一個synchornized關鍵字。
synchronized 會起到互斥效果, 某個線程執行到某個對象的 synchronized 中時, 其他線程如果也執行到同一個對象 synchronized 就會阻塞等待. 進入 synchronized 修飾的代碼塊, 相當于 加鎖 退出 synchronized 修飾的代碼塊, 相當于解鎖。
可以粗略理解成, 每個對象在內存中存儲的時候, 都存有一塊內存表示當前的 "鎖定" 狀態(類似于廁 所的 "有人/無人"). 如果當前是 "無人" 狀態, 那么就可以使用, 使用時需要設為 "有人" 狀態. 如果當前是 "有人" 狀態, 那么其他人無法使用, 只能排隊
注意:
上一個線程解鎖之后, 下一個線程并不是立即就能獲取到鎖. 而是要靠操作系統來 "喚醒". 這 也就是操作系統線程調度的一部分工作.
假設有 A B C 三個線程, 線程 A 先獲取到鎖, 然后 B 嘗試獲取鎖, 然后 C 再嘗試獲取鎖, 此時 B 和 C 都在阻塞隊列中排隊等待. 但是當 A 釋放鎖之后, 雖然 B 比 C 先來的, 但是 B 不一定就能獲取到鎖, 而是和 C 重新競爭, 并不遵守先來后到的規則.?
從而可以通過該代碼去解決問題?
class Counter{private int count = 0;synchronized public void increase(){count++;}public int getCount() {return count;}
}
public class Test4 {public static void main(String[] args) throws InterruptedException {Counter counter = new Counter();Thread t1 = new Thread(()->{for (int i = 0; i < 50000; i++) {counter.increase();}});Thread t2 = new Thread(()->{for (int i = 0; i < 50000; i++) {counter.increase();}});t1.start();t2.start();t1.join();t2.join();System.out.println(counter.getCount());}
}
synchronized
由上可知我們可以通過synchronized解決該線程不安全問題,但是具體使用和特質我們還是不清楚,這里詳細說下:?
synchronized的使用
1.最常見的是synchronized
?修飾代碼塊,并指定鎖對象。?
public class Counter {private int count = 0;private final Object lock = new Object(); // 鎖對象public void increment() {synchronized (lock) { count++;}}public int getCount() {return count;}
}
2.當?synchronized
?修飾實例方法時,鎖對象是當前實例(this
)。
?
public class Counter {private int count = 0;public synchronized void increment() {count++;}//該代碼約等于如下代碼public void increment() {synchronized(this){count++;}
}public int getCount() {return count;}
}?
-
多個線程調用同一個?
Counter
?實例的?increment()
?方法時,同一時間只有一個線程能夠執行該方法。 -
鎖對象是當前實例(
this
)。
3.當?synchronized
?修飾靜態方法時,鎖對象是當前類的?Class
類對象。對于一個類來說,只有一個唯一的calss類對象。
?
?
public class Counter {private int count = 0;public static synchronized void increment() {count++;}//該代碼約等于如下代碼public static void increment() {synchronized(Counter.class){count++;}
}public int getCount() {return count;}
}??
-
多個線程調用?
Counter.increment()
?方法時,同一時間只有一個線程能夠執行該方法。 -
鎖對象是?
Counter.class
。
synchronized
?的特性
互斥性
synchronized
?確保同一時間只有一個線程能夠執行被保護的代碼塊或方法。其他線程必須等待當前線程釋放鎖后才能獲取鎖并執行代碼。
這個我們之前就講述過了,這里不多講。
?鎖的可重入性?
static class Counter {public int count = 0;synchronized void increase() {count++;}synchronized void increase2() {increase();} }
如果用該代碼,按照之前對于鎖的設定, 第二次加鎖的時候, 該線程就會阻塞等待. 直到第一次的鎖被釋放, 才能獲取到第二個鎖. 但是釋放第一個鎖也是由該線程來完成, 結果這個線程已經堵塞了, 也就無法進行解鎖操作. 這時候就會 死鎖.
這樣的鎖稱為 不可重入鎖.
那么可重入鎖就是
一個對象可以多次在同一個線程內連續加鎖,而不會導致死鎖。
在同一個線程連續加鎖時,每次加鎖,鎖的計數器加 1;每次釋放鎖時,計數器減 1。只有當計數器為 0 時,鎖才會被完全釋放。
死鎖的情況
?雖然在synchronized中連續加鎖不會出現死鎖,但還有其他很多情況會出現死鎖,
?比如嵌套鎖導致的死鎖
public class NestedLockDeadlock {// 定義兩個鎖對象private static final Object lock1 = new Object();private static final Object lock2 = new Object();// 線程1:先獲取lock1,再獲取lock2public static void thread1() {synchronized (lock1) { // 獲取lock1System.out.println("Thread 1: Acquired lock1");try {Thread.sleep(100); // 模擬一些操作} catch (InterruptedException e) {e.printStackTrace();}synchronized (lock2) { // 嘗試獲取lock2System.out.println("Thread 1: Acquired lock2");}}}// 線程2:先獲取lock2,再獲取lock1public static void thread2() {synchronized (lock2) { // 獲取lock2System.out.println("Thread 2: Acquired lock2");try {Thread.sleep(100); // 模擬一些操作} catch (InterruptedException e) {e.printStackTrace();}synchronized (lock1) { // 嘗試獲取lock1System.out.println("Thread 2: Acquired lock1");}}}public static void main(String[] args) {// 創建兩個線程Thread t1 = new Thread(NestedLockDeadlock::thread1);Thread t2 = new Thread(NestedLockDeadlock::thread2);// 啟動線程t1.start();t2.start();}
}
?運行這段代碼后,程序會陷入死鎖,輸出類似如下內容:
Thread 1: Acquired lock1
Thread 2: Acquired lock2
死鎖原因
-
線程1 先獲取了
lock1
,然后試圖獲取lock2
。 -
線程2 先獲取了
lock2
,然后試圖獲取lock1
。 -
此時,線程1和線程2都在等待對方釋放鎖,但它們又都持有對方需要的鎖,導致死鎖。
?如何避免死鎖
那么如何避免死鎖呢?有四個原因導致死鎖,我們從這解決問題
避免死鎖問題只需要打破上述四點的其中一點即可,對于第一點和第二點對于Java中是打破不了的,他們都是synchronized的基本特性
從第三點來看,不要讓鎖嵌套獲取即可(但是有的時候必須嵌套,那就破除循環等待)
第四點破除循環等待:約定好加鎖的順序,讓所有的線程都按照約定要的順序來獲取鎖。
?內存可見性
我們在最開始講到線程安全的時候,聊到了關于線程安全問題總共有五種原因,前面我們講到了三種,還要兩種沒有涉及到,那么就來聊聊內存可見性引起的線程安全問題。
內存可見性問題指的是在一個線程修改了共享變量的值之后,其他線程是否能夠立即看到(即“看到”最新值)這個修改。如果不能,就可能出現內存可見性問題。
public class ThreadDemon26 {public static int flag = 0;public static void main(String[] args) {Thread t1 = new Thread(()->{while(flag == 0){//等待t1線程輸入flag的值,只要不為0就能結束t1線程}System.out.println("t1線程結束");});Thread t2 = new Thread(()->{System.out.println("請輸入flag的值");Scanner scanner = new Scanner(System.in);flag = scanner.nextInt();});t1.start();t2.start();}
}
從之前的內容可知兩個線程都寫的情況會造成線程安全問題,那么這段代碼有一個線程在寫,一個線程在讀會不會造成線程安全問題?
答案是會的,內存可見性會導致該問題
那么兩個線程都進行讀會造成線程安全問題?這里的答案是不會。
這段代碼想要表現出來的效果是,t1,t2線程同時運行,通過t2線程中輸入的flag的值來控制t1線程是否結束。
可是上文我們先后輸入了1,0,2......都沒能使t1線程結束,這是為什么呢?
我們看while(flag == 0){};這條語句其實有兩個指令
①load
cpu從內存中讀取flag的值(load)到cpu的寄存器上②訪問寄存器
cpu訪問寄存器中存儲的flag的值,與0進行比較? ? ??
①中load的操作(讀內存),相較于②中訪問寄存器的操作,開銷要大很多。(訪問寄存器的速度是讀內存的一萬倍)上述while循環中①②這兩條指令整體看,執行的速度非常快,等你scanner幾秒鐘了,我while循環中①②可能都執行幾億次了(cpu的計算能力非常強)
此時JVM就會懷疑,這個①號load 的操作是否還有存在的必要(節省開銷),于是經過load試探很多次發現都是一樣的,JVM索性就認為load 的值一直都一樣(速度太快了,等不到我們scanner輸入flag的值),在load一次后,寄存器保存了它的值,然后把load這個操作給優化掉,只留一個訪問寄存器的操作指令,訪問之前寄存器中保存的值,大大提高循環的執行速度。這就是內存可見化問題會出現的本質原因。
那么怎么解決該問題呢?我們就用volatile關鍵詞修飾變量。?
volatile關鍵詞
對于JVM的優化,都適用于單線程,但不適用于多線程,可能會出現bug。
而volatile關鍵字,是強制性關閉JVM優化,開銷是變大了,但是數據更準了。
volatile都是用來修飾變量的
功能①:解決內存可見性問題,每次訪問被volatile修飾的變量都要讀取內存,而不是優化到寄存器或者緩存器當中
功能②:禁止指令重排序,對于被volatile修飾的變量的操作指令,是不能被重排序的(這個等會會講)
對于線程指令是否會發生JVM的優化,我們程序員也很難判定是否發生了,所以更需要通過volatile去避免這種可能存在的問題。
volatile 和 synchronized 有著本質的區別. synchronized 能夠保證原子性, volatile 保證的是內存可見性.?
?指令重排序
指令重排序也是一種在編譯器發生的優化過程,它改變了程序原有的指令執行順序,使程序變得更好。
這在單線程是沒問題的,但是在多線程可能會導致bug,所以在多線程中我們需要解決該問題,就要用到volatile修飾重排序操作指令涉及的變量,這樣就沒問題了。
對于指令重排序問題的相關代碼后面講單例模式會有所涉及到。
6.wait和notify
由于線程之間是搶占式執行的, 因此線程之間執行的先后順序難以預知. 但是實際開發中有時候我們希望合理的協調多個線程之間的執行先后順序.
完成這個協調工作, 主要涉及到三個方法 wait() / wait(long timeout): 讓當前線程進入等待狀態. notify() / notifyAll(): 喚醒在當前對象上等待的線程.
wait()
wait 做的事情:
1.使當前執行代碼的線程進行等待. (把線程放到等待隊列中)
2.釋放當前的鎖(釋放后就可以允許其他線程用該鎖了)
3.滿足一定條件時被喚醒, 重新嘗試獲取這個鎖
?wait 要搭配 synchronized 來使用,脫離 synchronized 使用 wait 會直接拋出異常.
wait 結束等待的條件:
1.其他線程調用該對象的 notify 方法.
2.wait 等待時間超時 (wait 方法提供一個帶有 timeout 參數的版本, 來指定等待時間).
3.其他線程調用該等待線程的 interrupted 方法, 導致 wait 拋出 InterruptedException 異常.
這樣在執行object.wait()之后就一直等待下去,那么程序肯定不能一直這么等待下去了。這個時候就 需要使用到了另外一個方法喚醒的方法notify()。
notify()
notify 方法是喚醒等待的線程.
方法notify()也要在synchronized中調用,該方法是用來通知那些可能等待該對象的對象鎖的 其它線程,對其發出通知notify,并使它們重新獲取該對象的對象鎖。
如果notify和wait要聯動,必須要求notify的調用對象,notify的鎖對象,wait的調用對象,wait的鎖對象都必須相同。
如果有多個線程等待,則有線程調度器隨機挑選出一個呈 wait 狀態的線程。(并沒有 "先來后到")
在notify()方法后,當前線程不會馬上釋放該對象鎖,要等到執行notify()方法的線程將程序執行完,也就是退出synchronized之后才會釋放對象鎖。 ?
下面是一個案例?
static class WaitTask implements Runnable {private Object locker;public WaitTask(Object locker) {this.locker = locker;}@Overridepublic void run() {synchronized (locker) {while (true) {try {System.out.println("wait 開始");locker.wait();System.out.println("wait 結束");} catch (InterruptedException e) {e.printStackTrace();}}}}
}
static class NotifyTask implements Runnable {private Object locker;public NotifyTask(Object locker) {this.locker = locker;}@Overridepublic void run() {synchronized (locker) {System.out.println("notify 開始");locker.notify();System.out.println("notify 結束");}}
}
public static void main(String[] args) throws InterruptedException {Object locker = new Object();Thread t1 = new Thread(new WaitTask(locker));Thread t2 = new Thread(new NotifyTask(locker));t1.start();Thread.sleep(1000);t2.start();
}
notifyall()?
notify方法只是隨機喚醒某一個等待線程. 使用notifyAll方法可以一次喚醒所有的等待線程.
notifyAll()
比notify()
更安全,因為它不會隨機選擇一個線程喚醒,而是讓所有線程都有機會重新競爭鎖,從而避免了某些線程被永久忽略的問題。所以在大多數場景中,推薦使用notifyAll()
注意:雖然是同時喚醒所有線程, 但是這些線程需要競爭鎖. 所以并不是同時執行, 而仍然是有先有后的依次執行.
?
static class WaitTask implements Runnable {private Object locker;public WaitTask(Object locker) {this.locker = locker;}@Overridepublic void run() {synchronized (locker) {while (true) {try {System.out.println("wait 開始");locker.wait();System.out.println("wait 結束");} catch (InterruptedException e) {e.printStackTrace();}}}}
}
static class NotifyTask implements Runnable {private Object locker;public NotifyTask(Object locker) {this.locker = locker;}@Overridepublic void run() {synchronized (locker) {System.out.println("notify 開始");locker.notifyall();System.out.println("notify 結束");}}
}
public static void main(String[] args) throws InterruptedException {Object locker = new Object();Thread t1 = new Thread(new WaitTask(locker));Thread t3 = new Thread(new WaitTask(locker));Thread t4 = new Thread(new WaitTask(locker));Thread t2 = new Thread(new NotifyTask(locker));t1.start();t3.start();t4.start();Thread.sleep(1000);t2.start();
}?
wait(long time)?
wait(long time)
是 Java 中Object
類的一個方法,用于使當前線程在指定的時間內等待某個對象的監視器。如果在指定時間內沒有被喚醒,線程將自動從wait
狀態中退出。這個方法是Object
類的wait()
方法的超時版本,允許線程在等待時設置一個最大等待時間。時間是以毫秒為單位。
wait 和 sleep 的對比
其實理論上 wait 和 sleep 完全是沒有可比性的,因為一個是用于線程之間的通信的,一個是讓線程阻塞一段時間,唯一的相同點就是都可以讓線程放棄執行一段時間。
不同點在于:
wait 需要搭配 synchronized 使用,?sleep 不需要.
wait 是 Object 的方法 ,sleep 是 Thread 的靜態方法
7.單例模式
單例模式是一種經典的設計模式,是校招中最常考的設計模式之一.
那么啥是設計模式呢?
軟件開發中有很多常見的 “問題場景”. 針對這些問題場景, 大佬們總結出了一些固定的套路. 按照這個套路來實現代碼, 就不會吃虧,起碼能保下限。這些套路就是設計模式
那么什么是單例模式呢?單例模式是 Java 中最簡單的設計模式之一。單例模式 =>只允許存在單個實例.
如何保證這個類只有一個實例呢?靠程序猿口頭保證是否可行?比如, 你在類上寫一個注釋: 該類只能 new 一個實例,不能 new 多次。這完全在依賴人,人 一定是不靠譜的!!!所以需要讓編譯器來幫我們做一個強制的檢查通過一些編碼上的技巧,使編譯器可以自動發現咱們的代碼中是否有多個實例并且在嘗試創建多個實例的時候,直接編譯出錯,根本上保證對象是唯一實例.=>這樣的代碼,就稱為 單例模式?
下面是一個簡單的單例模式代碼:
public class Singleton{private static final Singleton singleton = new Singleton();private Singleton(){}public static Singleton getInstance() {return singleton;}
}
通過精巧的代碼設計,就可以達到只允許存在一個實例對象。通過Singletion.getinstance()可以得到唯一的實例對象。?
除該類型代碼以外,還有另一種類型的代碼,下面講述一下。
單例模式有兩種類型:
- 懶漢式:在真正需要使用對象時才去創建該單例類對象
public class Singleton {private static Singleton singleton;private Singleton(){}public static Singleton getInstance() {if (singleton == null) {singleton = new Singleton();}return singleton;}}
- 餓漢式:在類加載時已經創建好該單例對象,等待被程序使用(最上面的代碼就是)
對于懶漢式的代碼因為是真正要用的時候才創建對象,所以相對于餓漢式來說開銷更小,效率更高。
但是雖然效率更高,在多線程中餓漢卻比懶漢更加安全,懶漢會觸發線程安全問題。下面請看分析。
單例模式中多線程引發的安全問題?
?兩個線程同時調用懶漢的單例模式中的Singletion.getinstance()和兩個線程同時調用餓漢的單例模式中的Singletion.getinstance(),哪個會有bug?
餓漢都是讀取,由于讀讀不會觸發線程安全,所以餓漢不會引發線程安全問題。
懶漢兩個線程都涉及到了修改,由于并發執行,導致可能出現兩個對象同時存在,不符合單例模式,發生線程安全問題。
?最容易想到的解決方法就是在方法上加鎖,或者是對類對象加鎖,程序就會變成下面這個樣子
public static synchronized Singleton getInstance() {if (singleton == null) {singleton = new Singleton();}return singleton;
}
// 或者
public static Singleton getInstance() {synchronized(Singleton.class) { if (singleton == null) {singleton = new Singleton();}}return singleton;
}
這樣就規避了兩個線程同時創建Singleton對象的風險,但是引來另外一個問題:每次去獲取對象都需要先獲取鎖,鎖的開銷是很大的,可以說有鎖的線程雖然安全,但注定不會高性能,極端情況下,可能會出現卡頓現象。
所以接下來要做的就是優化性能,我們發現,當創建好了對象后不獲取鎖也不會引發線程安全問題,只有第一次沒有對象的時候不獲取鎖才會引發線程安全問題。所以只有第一次創建對象時才需要加鎖,我們就將代碼設計成如下邏輯:如果沒有實例化對象則加鎖創建,如果已經實例化了,則不需要加鎖,直接獲取實例。這樣就能減少獲取鎖的次數
所以直接在方法上加鎖的方式就被廢掉了,因為這種方式無論如何都需要先獲取鎖,開銷極大,我們改善后的代碼如下:
public static Singleton getInstance() {if (singleton == null) { // 線程A和線程B同時看到singleton = null,如果不為null,則直接返回singletonsynchronized(Singleton.class) { // 線程A或線程B獲得該鎖進行初始化if (singleton == null) { // 其中一個線程進入該分支,另外一個線程則不會進入該分支singleton = new Singleton();}}}return singleton; }
上面的代碼已經完美地解決了并發安全+性能低效問題:
第2行代碼,如果singleton不為空,則直接返回對象,不需要獲取鎖;而如果多個線程發現singleton為空,則進入內部獲取鎖;
第3行代碼,多個線程嘗試爭搶同一個鎖,只有一個線程爭搶成功,第一個獲取到鎖的線程會再次判斷singleton是否為空,因為singleton有可能已經被之前的線程實例化
其它之后獲取到鎖的線程在執行到第4行校驗代碼,發現singleton已經不為空了,則不會再new一個對象,直接返回對象即可
之后所有進入該方法的線程都不會去獲取鎖,在第一次判斷singleton對象時已經不為空了
上面這段代碼已經近似完美了,但是還存在最后一個問題:指令重排序問題(我們之前講過)?
單例模式中出現的指令重排序問題?
創建一個對象,在JVM中會經過三步:
(1)為singleton分配內存空間
(2)初始化singleton對象
(3)將singleton指向分配好的內存空間
指令重排序是指:JVM在保證最終結果正確的情況下,可以不按照程序編碼的順序執行語句,盡可能提高程序的性能。
在這三步中,第2、3步有可能會發生指令重排現象,創建對象的順序變為1-3-2,會導致多個線程獲取對象時,有可能線程A創建對象的過程中,執行了1、3步驟,此時經過線程的調度執行線程B,線程B判斷singleton已經不為空,獲取到未初始化的singleton對象,就會報N異常。
?這在單線程是不會出現該問題,多線程會出現。
使用volatile關鍵字可以防止指令重排序,?所以通過volatile修飾跟創建對象有關的變量則可以阻止該問題發生。
public class Singleton {private static volatile Singleton singleton;private Singleton(){}public static Singleton getInstance() {if (singleton == null) { // 線程A和線程B同時看到singleton = null,如果不為null,則直接返回singletonsynchronized(Singleton.class) { // 線程A或線程B獲得該鎖進行初始化if (singleton == null) { // 其中一個線程進入該分支,另外一個線程則不會進入該分支singleton = new Singleton();}}}return singleton;}}
但無論是完美的懶漢式還是餓漢式,終究敵不過反射和序列化,它們倆都可以把單例對象破壞掉(產生多個對象)。但由于反射和序列化在該情況中用的極少,所以這里就沒必要詳細講述,只需要清楚就行。
8.阻塞隊列
阻塞隊列概念?
阻塞隊列是一種特殊的隊列,同樣遵循“先進先出”的原則,支持入隊操作和出隊操作和一些基礎方法。在此基礎上,阻塞隊列會在隊列已滿或隊列為空時陷入阻塞,所以它具有如下特性:
1.當隊列已滿時,繼續入隊列就會阻塞,直到有其他線程從隊列中取走元素。
2.當隊列為空時,繼續出隊列也會阻塞,直到有其他線程向隊列中插入元素。3.并且它是線程安全的,就是多線程中使用它是不會引發線程安全bug的
那么我們用它是干嘛呢?一般用它實現生產者消費者模型,對于該概念我們下面詳細說下:
生產者消費者模型
生產者消費者模型有兩種角色,生產者和消費者,兩者之間通過緩沖容器來達到解耦合和削峰填谷的效果。類似于廠商和客戶與中轉倉庫之間的關系,如下圖:
廠家生產的商品堆積在中轉倉庫,當中轉倉庫滿時,入倉阻塞,當中轉倉庫為空時,出倉阻塞。通過上述結構,生產者和消費者擺脫了“產銷一體”的運作模式,即解耦合。同時,無論是客戶需求暴增,還是廠家產量飆升,都會被中央倉庫協調,避免突發情況導致結構崩潰,達到削峰填谷的作用。
同理,根據生產者消費者模型,我們將線程帶入到消費者和生產者的角色,阻塞隊列帶入到緩沖空間的角色,一個類似的模型很容易就搭建起來了。
所以說,阻塞隊列對生產者消費者模型是相當重要的。
阻塞隊列的作用
①解耦合
作為生產者消費者模式的緩沖空間,將線程(其他)之間分隔,通過阻塞隊列間接聯系起來,起到降低耦合性的作用,這樣即使其中一個掛掉,也不會使另一個也跟著掛掉。(就是降低它們之間的聯系性)
②削峰填谷
因為阻塞隊列本身的大小是有限的,所以能起到一個限制作用,即在消費者面對突發暴增的入隊操作,依然不受影響。
如電商平臺在每年雙十一時都會出現請求峰值的情況,如下:
而假設電商平臺對請求的處理流程是這樣的:
因為處理請求需要消耗硬件資源,如果沒有消息隊列,面對雙十一這種請求暴增的情況,請求處理服務器很可能就直接掛掉了。
而有了消息隊列之后,請求處理服務器不必直接面對大量請求的沖擊,仍舊可以按原先的處理速度來處理請求,避免了被沖爆,這就是‘削峰’。
沒有被處理的請求也不是不處理了,而是當消息隊列有空閑時再繼續流程,即高峰請求被填在低谷中,這就是‘填谷’。
經過‘削峰填谷’之后的請求處理曲線就(大致)變成了下圖:
?經過阻塞隊列的請求量就相比之前穩定很多了
阻塞隊列的使用
在 Java?標準庫中就提供了現成阻塞隊列這樣的數據結構:BlockingQueue ,這里?BlockingQueue?是一個接口,實現這個接口的類也有很多:
ArrayBlockingQueue
: 基于數組的阻塞隊列。
LinkedBlockingQueue
: 基于鏈表的阻塞隊列。
PriorityBlockingQueue
: 支持優先級的阻塞隊列。阻塞隊列一般用put和take方法。
put 方法用于阻塞式的入隊列, take 用于阻塞式的出隊列. BlockingQueue 也有 offer, poll, peek 等方法, 但是這些方法不帶有阻塞特性,所以不用
阻塞隊列的實現?
在了解了阻塞隊列的使用后,我們就準備實現一個阻塞隊列來加深對阻塞隊列的理解
?實現阻塞隊列,我們可以從淺到深的來實現,先實現一個普通隊列,再在普通隊列的基礎上,添加上線程安全,再增加阻塞功能,那么就來普通隊列的實現吧。這里我們實現一個環形隊列(之前講過怎么實現,這里直接給代碼)?
class MyBlockingQueue {//對象公用鎖private Object lock = new Object();//String類型的數組,存儲隊列元素private String[] elems = null;//隊首位置private int head = 0;//隊尾位置private int tail = 0;//存儲的元素個數private int size = 0;//構造方法,用于構建定長數組,數組長度由參數指定public MyBlockingQueue(int capacity) {elems = new String[capacity];}//入隊方法public void put(String elem) throws InterruptedException {synchronized(lock) {//已滿時入隊操作阻塞while(size == elems.length) {lock.wait();}//將元素存入隊尾elems[tail] = elem;//存入后,隊尾位置后移一位tail++;//實現環形隊列的關鍵,超過數組長度后回歸數組首位if(tail >= elems.length) {//回歸數組首位tail = 0;}//存入后元素總數加一size++;//當出隊操作阻塞時,入隊后為其解除阻塞//(入隊后隊列不為空了)lock.notify();}}//出隊方法public String tack() throws InterruptedException {//存儲取出的元素,默認為nullString elem = null;synchronized (lock) {//隊列為空時出隊操作阻塞while (size == 0) {lock.wait();}//出隊,取出隊首值(不用置空,隊尾存入時覆蓋)elem = elems[head];//出隊后,隊首位置后移一位head++;//實現環形隊列的關鍵,超過數組長度后回歸數組首位if(head == elems.length) {//回歸數組首位head = 0;}//存入后元素總數加一size--;//當入隊操作阻塞時,出隊后為其解除阻塞//(出隊后隊列不滿)lock.notify();}//返回取出的元素return elem;}
}
之后我們要將其變為阻塞隊列,就要改進該代碼。
首先由于阻塞隊列是線程安全的,所以要用volatile修飾變量,sychronized修飾take和put方法。
然后對于滿了或者空了會導致阻塞情況,我們就用wait()去阻塞,notify()則放在take和put方法的最后面去喚醒。
那么現在代碼還有問題嗎?還是存在的,wait()除了用notify()喚醒還可以用什么喚醒?
還可以用interrupt去強制喚醒并拋出異常,如果用的話此時阻塞隊列是滿的且退出了if循環,并且讓size再加一,此時就會引發bug,所以我們的if必須換成while,這樣就不能退出循環,并且繼續阻塞。
此時阻塞隊列就是沒有問題的了。
阻塞隊列完全版:
class MyBlockingQueue {private String[] elems = null;private volatile int head = 0;private volatile int tail = 0;private volatile int size = 0;public MyBlockingQueue(int capacity) {elems = new String[capacity];}public synchronized void put(String elem) {while(size == elems.length) {try{this.wait();}catch (Exception e){e.printStackTrace();}}elems[tail] = elem;tail++;if(tail >= elems.length) {tail = 0;}size++;this.notify();}public synchronized String take() {String elem = null;while(size == 0) {try{this.wait();}catch (Exception e){e.printStackTrace();}}elem = elems[head];head++;if(head == elems.length) {head = 0;}size--;this.notify();return elem;}
}
class ThreadDemo5 {public static void main(String[] args) throws InterruptedException {MyBlockingQueue myBlockingQueue = new MyBlockingQueue(100);Thread customer = new Thread(()->{while (true) {try {String elem = myBlockingQueue.take();System.out.println("消費元素:-> " + elem);Thread.sleep(500);} catch (InterruptedException e) {throw new RuntimeException(e);}}},"消費者");Thread producer = new Thread(()->{int n = 0;while (true) {try {myBlockingQueue.put(n + "");System.out.println("生產元素:-> " + n);} catch (Exception e) {throw new RuntimeException(e);}n++;}},"生產者");// 啟動生產者與消費者線程customer.start();producer.start();}
}
9.定時器
定時器是軟件開發中的一個重要組件. 類似于一個 "鬧鐘". 達到一個設定的時間之后, 就執行某個指定好的代碼.
定時器的使用?
標準庫中的定時器 標準庫中提供了一個 Timer 類. Timer 類的核心方法為 schedule . schedule 包含兩個參數.
第一個參數是繼承timetask抽象類的類實例且內部重寫了run方法:指定即將要執行的任務代碼(timetask實現了runable接口所以有run方法)
?第二個參數指定多長時間之后執行 (單位為毫秒).
Timer timer = new Timer(); timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("hello");} }, 3000);
執行schedule方法的時候,系統把要執行的任務放到timer對象中,與此同時timer對象里頭自帶一個線程叫做“掃描線程”,一旦時間到掃描線程就會執行剛才安排的任務,執行完所有任務后線程也不會銷毀,會阻塞等待直到其他的任務被放到timer對象中再繼續執行(就這么重復)
?定時器的實現
對于定時器來說,重要的還是怎么用它,實現它主要是加深對它的理解。 所以這里的實現過程邏輯細節等我就不講了,強烈推薦一個文章,寫的很好。
JavaEE 初階(13)——多線程11之“定時器”_jeecg 定時器-CSDN博客
總代碼:
import java.util.PriorityQueue;class MyTimerTask implements Comparable<MyTimerTask> {//此處這里的 time,通過毫秒時間戳,表示這個任務具體啥時候執行private long time;private Runnable runnable;public MyTimerTask(Runnable runnable, long delay) {this.time = System.currentTimeMillis() + delay;this.runnable = runnable;}public void run() {runnable.run();}public long getTime() {return time;}@Overridepublic int compareTo(MyTimerTask o) {//比如,當前時間是 10:30,任務時間是 12:00,不應該執行//如果當前時間是 10:30,任務時間是 10:29,應該執行//誰減去誰,可以通過實驗判斷return (int) (this.time - o.time);}
}public class MyTimer {private final PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();private final Object locker = new Object();public MyTimer() {Thread t = new Thread(() -> {try {while (true) {synchronized (locker) {while (queue.isEmpty()) {//如果還沒添加任務,會不斷循環執行判斷,出現線程餓死。//continue;//因此,使用wait等待,當添加任務后喚醒locker.wait();}MyTimerTask task = queue.peek();if (System.currentTimeMillis() >= task.getTime()) {task.run();queue.poll();} else {//如果還沒到任務執行時間,依舊不斷循環判斷,出現線程餓死。//continue;//因此,使用有等待期限的 wait,計算執行的時間與當前時間的差值//當添加新的任務后,wait 被喚醒,再進行新的判斷locker.wait(task.getTime() - System.currentTimeMillis());}}}} catch (InterruptedException e) {e.printStackTrace();}});t.start();}public void schedule(Runnable runnable, long delay) {synchronized (locker) {MyTimerTask task = new MyTimerTask(runnable, delay);queue.offer(task);// 喚醒 waitlocker.notify();}}
}
10.線程池
為什么使用線程池
在前面我們都是通過new Thread() 來創建線程的,雖然在java中對線程的創建、中斷、銷毀、等值等功能提供了支持,但從操作系統角度來看,如果我們頻繁的創建和銷毀線程,是需要大量的時間和資源的,那么有沒有什么開銷更小的方法?
第一種是協程,它可以說是輕量級線程,但是java很少用,多用于go和python。
第二種是線程池,java中多用線程池去解決頻繁的創建和銷毀線程問題。
那么為啥引入線程池就能夠提升效率呢?
1.直接創建/銷毀線程,是需要在用戶態+內核態配合完成的工作,對于線程池,只需要在用戶態即可,不需要內核態的配合,這樣開銷就更小
2.等線程用完之后,線程池不會銷毀該線程,而是讓其阻塞,等下次用的時候會再次利用它,所以不用頻繁的進行創建和銷毀。
線程池最核心的設計思路:復用線程,平攤線程的創建與銷毀的開銷代價
線程池的使用
java 提供了多種方式來創建線程池,主要通過
Executors
工廠類或直接使ThreadPoolExecutor
類來完成
?工廠類Executors(工廠模式)
使用Executors工廠類:
newFixedThreadPool(int nThreads):創建一個固定大小的線程池,線程數量由nThreads參數確定。
newCachedThreadPool():創建一個線程數量為動態的線程池,線程數量會根據任務數量動態變化,當長時間沒有新任務時,空閑線程會被終止。newSingleThreadExecutor():創建一個單線程的線程池,它只會創建一個線程來執行任務。
newScheduledThreadPool(int corePoolSize):創建一個可以安排任務的線程池,可以指定延遲執行任務或定期執行任務。后面兩個我們用的都不多,主要是用前面兩個
下面是使用代碼:
? ? import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors;public class ThreadPoolExample {public static void main(String[] args) {// 創建一個固定大小的線程池ExecutorService fixedThreadPool = Executors.newFixedThreadPool(4);// 創建一個可緩存的線程池(線程數量動態調整)ExecutorService cachedThreadPool = Executors.newCachedThreadPool();} }??
這代碼我們有個疑點,我們并沒有new一個對象,那我們是怎么創建出來對象的呢?
這個問題涉及到工廠模式這種設計模式:
工廠模式是一種常用的設計模式,用于封裝對象的創建邏輯。它通過使用方法來創建對象(new在方法內部),而不是直接使用
new
關鍵字實例化對象。這樣可以將對象的創建邏輯與使用邏輯解耦,提高代碼的可維護性和可擴展性。這里就是用方法創建出對象,所以涉及到了工廠模式
?ThreadPoolExecutor類(直接new)
對于剛才講的 Executors 本質上是 ThreadPoolExecutor 類的封裝.? ? ? ? ?
而對于ThreadPoolExecutor類本身我們提供了更多的可選參數, 可以進一步細化線程池行為的設定.?
如下圖是?ThreadPoolExecutor類的構造方法:
?
-
核心線程數(
corePoolSize
):線程池中始終保持的線程數量。這是不會被銷毀的。 -
最大線程數(
maximumPoolSize
):線程池中允許的最大線程數量。這種一般涉及到剛才的動態線程池,如果任務多了則創建一些線程,少了的話過了一段時間則會銷毀,但核心線程數不變。 -
空閑線程存活時間(
keepAliveTime
):當線程池中的線程數量超過核心線程數時,空閑線程的存活時間。 -
任務隊列(
workQueue
):其為阻塞隊列,用于存儲等待執行的任務。要記住,當我們創建線程池時,系統也會同時自動創建一個阻塞隊列去存儲等待執行的任務,這樣效率就更高。 -
線程工廠(
threadFactory
):線程工廠是一個用于創建線程的工具類或接口,它允許用戶自定義線程的創建邏輯,開發者可以控制線程的名稱、優先級、異常處理等屬性,從而更好地管理線程資源。 -
拒絕策略(
handler
):當線程池已滿且阻塞隊列也已滿時,新任務的處理策略。
?下面重點講述一下拒絕策略:
AbortPolicy
:直接拋出RejectedExecutionException
異常。
CallerRunsPolicy
:由提交任務的線程直接執行任務。
DiscardPolicy
:直接丟棄任務,不拋出異常。
DiscardOldestPolicy
:丟棄隊列中最老的任務,然后嘗試提交新任務。
下面是其創建代碼?
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2, // 核心線程數4, // 最大線程數60, // 空閑線程存活時間TimeUnit.SECONDS, // 時間單位new ArrayBlockingQueue<>(10), // 任務隊列,容量為 10Executors.defaultThreadFactory(), // 線程工廠new ThreadPoolExecutor.AbortPolicy() // 拒絕策略);
總結一下:
工廠模式創建線程:適合簡單的線程池創建場景,代碼簡單,但靈活性有限。
構造方法創建線程:適合需要靈活配置線程池屬性的場景,通過自定義線程池,可以更好地管理線程資源,提高代碼的可維護性和可擴展性。
submit?
?通過線程池.submit(繼承runable的類的對象) 可以提交一個任務到線程池中執行.
ExecutorService pool = Executors.newFixedThreadPool(10); pool.submit(new Runnable() {@Overridepublic void run() {System.out.println("hello");} });
?實現一個線程池
?這里就直接上代碼了,不多說,重點還是使用線程池,不是實現線程池。
/*** 自定義線程池執行器類* 該類通過實現一個具有固定大小的線程池和一個阻塞隊列來管理線程,用于異步執行任務*/
class MyThreadPoolExecutor {// 創建阻塞隊列,用于存放待執行的任務// 隊列大小設為1000,用于控制并發任務的數量,避免過多任務導致資源耗盡BlockingQueue<Runnable> blockingQueue=new ArrayBlockingQueue<>(1000);/*** 構造函數,初始化線程池* 創建一個線程,該線程循環從阻塞隊列中取任務并執行* 這個線程是線程池中的工作線程,負責執行提交的任務*/public MyThreadPoolExecutor(int n) {for (int i = 1; i <= n; i++) {Thread t = new Thread(() -> {// 無限循環,確保線程池可以持續處理任務,直到程序中斷或阻塞隊列被清空while (true) {try {// 從阻塞隊列中取出一個任務,如果隊列為空,則線程被阻塞,直到有任務放入隊列Runnable task = blockingQueue.take();// 執行取出的任務task.run();} catch (InterruptedException e) {// 如果線程在等待狀態時被中斷,拋出運行時異常// 這通常會導致程序異常終止throw new RuntimeException(e);}}});// 啟動線程池中的工作線程t.start();}}/*** 提交一個任務到線程池* @param task 需要被執行的任務* 任務被放入阻塞隊列中,隨后由線程池中的工作線程執行*/public void submit(Runnable task){// 將任務放入阻塞隊列,如果隊列已滿,則操作會阻塞,直到有空間可用blockingQueue.offer(task);}
}
class DemoTest1{public static void main(String[] args) throws InterruptedException {MyThreadPoolExecutor ex=new MyThreadPoolExecutor(4);for(int i=0;i<100;i++) {int id = i;ex.submit(()->{System.out.println(Thread.currentThread().getName()+" 任務:"+id);});}}
}
?下圖為執行結果:
?