文章目錄
- 📕1. Thread類及常見方法
- ??1.1 創建線程
- ??1.2 Thread 的常見構造方法
- ??1.3 Thread 的幾個常見屬性
- ??1.4 啟動一個線程---start()
- ??1.5 中斷一個線程---interrupt()
- ??1.6 等待一個線程---join()
- ??1.7 獲取當前線程引用
- ??1.8 休眠當前線程
- 📕2. 線程的狀態
- 📕3. 線程安全
- ??3.1 線程不安全案例
- ??3.2 線程不安全的原因
- 📕4. synchronized 關鍵字
- ??4.1 synchronized的特性
- ??4.2 synchronized 使用示例
- 📕5. volatile關鍵字
- 📕6. wait與notify
- ??6.1 wait()方法
- ??6.2 notify()方法
- ??6.3 notifyAll()方法
- 📕7. 單例模式
- 📕8. 阻塞隊列
- ??8.1 生產者消費者模型
- ??8.2 標準庫中的阻塞隊列
- ??8.3 阻塞隊列實現
- 📕9. 定時器
- 📕10.線程池
- ??10.1 ExecutorService 和 Executors
- ??10.2 ThreadPoolExecutor
- ??10.3 線程池的工作流程
🌰首先,我們設想以下的一個場景:當一家公司去銀行辦理業務,既要進行財務轉賬,?要進行福利發放,還得進行繳社保。如果只有張三一個會計就會忙不過來,耗費的時間特別長。為了讓業務更快的辦理好,張三又找來兩位同事李四、王五?起來幫助他,三個人分別負責一個事情,分別申請一個號碼進行排隊,自此就有了三個執行流共同完成任務,但本質上他們都是為了辦理同一家公司的業務。
此時,我們就把這種情況稱為多線程,將一個大任務分解成不同小任務,交給不同執行流就分別排隊執行。其中李四、王五都是張三叫來的,所以張三?般被稱為主線程(Main Thread)。
為什么要有線程呢?
-
首先, “并發編程” 成為 “剛需” ,
單核 CPU 的發展遇到了瓶頸. 要想提高算力, 就需要多核 CPU. 而并發編程能更充分利用多核 CPU資源 . 有些任務場景需要 “等待 IO”, 為了讓等待 IO 的時間能夠去做?些其他的?作, 也需要用到并發編程. -
其次, 雖然多進程也能實現并發編程, 但是線程比進程更輕量
創建線程比創建進程更快.
銷毀線程比銷毀進程更快.
調度線程比調度進程更快 -
最后, 線程雖然比進程輕量, 但是人們還不滿足 , 于是又有了 “線程池”(ThreadPool) 和 “協程”
(Coroutine)
進程和線程的區別?
- 進程是包含線程的. 每個進程至少有一個線程存在,即主線程。
- 進程和進程之間不共享內存空間. 同一個進程的線程之間共享同一個內存空間.
- 進程是系統分配資源的最小單位,線程是系統調度的最小單位。
- 一個進程掛了一般不會影響到其他進程. 但是一個線程掛了, 可能把同進程內的其他線程一起帶走(整個進程崩潰).
Java 的線程 和 操作系統線程 的關系?
線程是操作系統中的概念. 操作系統內核實現了線程這樣的機制, 并且對用戶層提供了一些 API 供用戶
使用 , Java 標準庫中 Thread 類可以視為是對操作系統提供的 API 進行了進一步的抽象和封裝.
📕1. Thread類及常見方法
Thread 類是 JVM 用來管理線程的一個類,換句話說,每個線程都有一個唯一的Thread 對象與之關聯。
用我們上面的例子來看,每個執行流,也需要有一個對象來描述,類似下圖所示,而 Thread 類的對象就是用來描述一個線程執行流的,JVM 會將這些 Thread 對象組織起來,用于線程調度,線程管理。
??1.1 創建線程
- 繼承Thread類
//繼承Thread來創建一個線程類
class MyThread extends Thread{@Override//重新run方法,run方法中是該線程具體要做的任務public void run() {for (int i = 0; i < 100; i++) {System.out.println(i);}}
}
public class Test {public static void main(String[] args) {//實例化線程類對象MyThread t = new MyThread();//通過start()方法啟動線程t.start();}
}
- 實現 Runnable 接口
class MyRunnable implements Runnable{@Overridepublic void run() {for (int i = 0; i < 100; i++) {System.out.println(i);}}
}
public class Test {public static void main(String[] args){//創建 Thread 類實例, 調? Thread 的構造?法時將 Runnable 對象作為 target 參數Thread t = new Thread(new MyRunnable());t.start();}
}
- 匿名內部類創建 Thread 子類對象
public class Test {public static void main(String[] args) {//匿名內部類創建Thread的子類Thread t = new Thread(){@Overridepublic void run() {for (int i = 0; i < 100; i++) {System.out.println(i);}}};t.start();}
}
- 匿名內部類創建 Runnable 子類對象
public class Test {public static void main(String[] args) {Thread t = new Thread(new Runnable() {@Overridepublic void run() {for (int i = 0; i < 100; i++) {System.out.println(i);}}});t.start();}
}
- lambda 表達式創建 Runnable 子類對象
public class Test {public static void main(String[] args) {Thread t = new Thread(()->{System.out.println("這是一個用lambda表達式創建的線程");});t.start();}
}
強烈推薦!!!
??1.2 Thread 的常見構造方法
方法 | 說明 |
---|---|
Thread() | 創建線程對象 |
Thread(String name) | 創建線程對象并命名 |
Thread(Runnable target , String name) | 使用Runnable對象創建線程對象并命名 |
Thread(Runnable target) | 使用Runnable對象創建線程對象 |
??1.3 Thread 的幾個常見屬性
? ID 是線程的唯一標識,不同線程不會重復
? 名稱是什么無所謂,不影響運行,是為了方便調試
? 狀態表示線程當前所處的一個情況
? 優先級高的線程理論上來說更容易被調度到
? 關于后臺線程,需要記住一點:JVM會在一個進程的所有非后臺線程結束后,才會結束運行。
? 是否存活,即簡單的理解,為 run 方法是否運行結束了
??1.4 啟動一個線程—start()
我們現在已經知道如何通過覆寫 run 方法創建?個線程對象,但線程對象被創建出來并不意味著線程就開始運行了。
? 覆寫 run 方法是提供給線程要做的事情的指令清單
? 線程對象可以認為是把 李四、王五叫過來了
? 而調用 start() 方法,就是喊一聲:”行動起來!“,線程才真正獨立去執行了。
調用 start 方法, 才真的在操作系統的底層創建出一個線程.
??1.5 中斷一個線程—interrupt()
- 通過一個變量進行標記
public class Test {public static boolean flag = true;public static void main(String[] args) throws InterruptedException {Thread t = new Thread(){@Overridepublic void run() {while(flag){System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}};t.start();System.out.println("hello main");Thread.sleep(3000);flag = false;System.out.println("讓線程中斷");}
}
- 調用 interrupt() 方法
public class Test {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(()->{// 由于這個 currentThread 方法, 是在后續 t start 之后, 才執行的.// 并且是在 t 線程中執行的. 返回的結果就是指向 t 線程對象的引用了.while(!Thread.currentThread().isInterrupted()){System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();break;}}});t.start();Thread.sleep(2000);//調用這個方法,就是把標志位由false改為truet.interrupt();}
}
//使用interrupt()方法的時候
//1. t線程沒有進行sleep()阻塞時,t的isInterrupted()方法返回true,通過循環條件結束循環
//2. t線程進行sleep()阻塞時,t的isInterrupted()方法還是返回true,但是sleep()方法如果被提前喚醒,拋出InterruptedException異常,同時會把isInterrupted()方法設為false,此時就要手動決定是否要結束線程了
??1.6 等待一個線程—join()
有時,我們需要等待一個線程完成它的工作后,才能進行自己的下?步工作。例如,張三只有等李四轉賬成功,才決定是否存錢,這時我們需要一個方法明確等待線程的結束。
public class Test {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(()->{for (int i = 0; i < 4; i++) {System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});t.start();System.out.println("main線程開始了");t.join();System.out.println("main線程等t線程結束了");}
}
??1.7 獲取當前線程引用
??1.8 休眠當前線程
方法 | 解釋 |
---|---|
public static native void sleep(long millis) throws InterruptedException; | 休眠當前線程 , 以毫米為單位 |
📕2. 線程的狀態
線程的狀態是一個枚舉類型:
? NEW: 安排了工作, 還未開始行動
? RUNNABLE: 可工作的. 又可以分成正在工作中和即將開始工作
? BLOCKED: 由于加鎖產生的阻塞
? WAITING: 無超時時間的阻塞
? TIMED_WAITING:有超時時間的阻塞
? TERMINATED: 工作完成了
📕3. 線程安全
??3.1 線程不安全案例
請大家觀察下述代碼:
public class Test {private static int count;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(()->{for (int i = 0; i < 50_000; i++) {count++;}});Thread t2 = new Thread(()->{for (int i = 0; i < 50_000; i++) {count++;}});t1.start();t2.start();t1.join();t2.join();System.out.println(count);}
}
大家認為count最終的值會是100_000嗎? 不是的,count最終的值是一個小于100_000的隨機數.那為什么呢?
線程不安全的概念?
如果多線程環境下代碼運行的結果是符合我們預期的,即在單線程環境應該的結果,則說這個程序是線程安全的。
??3.2 線程不安全的原因
-
線程調度是隨機的
-
修改共享數據
即多個線程同時修改一個數據 -
原子性
什么是原子性?
我們把一段代碼想象成一個房間,每個線程就是要進入這個房間的?。如果沒有任何機制保證,A進入房間之后,還沒有出來;B 是不是也可以進入房間,打斷 A 在房間里的隱私。這個就是不具備原子性的。
那我們應該如何解決這個問題呢?是不是只要給房間加一把鎖,A 進去就把門鎖上,其他人是不是就進不來了。這樣就保證了這段代碼的原子性了。
有時也把這個現象叫做同步互斥,表示操作是互相排斥的。
一條 java 語句不一定是原子的,也不一定只是一條指令
上述代碼中的count++對應著3條指令:
- load : 從內存把數據讀到 CPU
- add : 進行數據更新
- save : 把數據寫回到 CPU
上述三條指令在多線程中就是有問題的指令.如果一個線程正在對一個變量操作,中途其他線程插入進來了,如果這個操作被打斷了,結果就可能是錯誤的。
將3種指令執行順序枚舉出我們發現:只有第一種和第二種是正確的
- 內存可見性
這里主要個大家介紹一下JMM模型,關于可見性內容請大家查閱目錄找volatile關鍵字
Java 內存模型 (JMM—Java Memory Model):
Java虛擬機規范中定義了Java內存模型 , 目的是屏蔽掉各種硬件和操作系統的內存訪問差異,以實現讓Java程序在各種平臺下都能達到一致的并發效果.
? 線程之間的共享變量存在 主內存 (Main Memory)
? 每一個線程都有自己的 “工作內存” (Working Memory)
? 當線程要讀取一個共享變量的時候, 會先把變量從主內存拷貝到工作內存, 再從工作內存讀取數據.
? 當線程要修改一個共享變量的時候, 也會先修改工作內存中的副本, 再同步回主內存.
因為每個線程有自己的工作內存, 這些工作內存中的內容相當于同?個共享變量的 “副本”. 這就導致了此時修改線程1 的工作內存中的值, 線程2 的工作內存不?定會及時變化.
初始情況下, 兩個線程的工作內存內容一致.
一旦線程1 修改了 a 的值, 此時主內存不一定能及時同步. 對應的線程2 的工作內存的 a 的值也不?定能及時同步
此時就引入了三個問題:
1.為什么要整這么多內存呢?
實際并沒有這么多 “內存”. 這只是 Java 規范中的一個術語, 是屬于 “抽象” 的叫法.
所謂的 “主內存” 才是真正硬件角度的 “內存”. 而所謂的 “工作內存”, 則是指 CPU 的寄存器和高速緩存.
CPU的寄存器和緩存統稱為工作內存,越往上,速度越快,空間越小,成本越高
2.為啥要這么麻煩的拷來拷去?
因為 CPU 訪問自身寄存器的速度以及高速緩存的速度, 遠遠超過訪問內存的速度(快了 3 - 4 個數量級,也就是幾千倍, 上萬倍).
比如某個代碼中要連續 10 次讀取某個變量的值, 如果 10 次都從內存讀, 速度是很慢的. 但是如果只是第一次從內存讀, 讀到的結果緩存到 CPU 的某個寄存器中, 那么后 9 次讀數據就不必直接訪問內存了.效率就大大提高了.
3.那么接下來問題又來了, 既然訪問寄存器速度這么快, 還要內存干啥??
答案就是?個字: 貴 , 快和慢都是相對的. CPU 訪問寄存器速度遠遠快于內存, 但是內存的訪問速度?遠遠快于硬盤.
對應的, CPU 的價格最貴, 內存次之, 硬盤最便宜.
- 指令重排序
一段代碼是這樣的:
- 去前臺取下 U 盤
- 去教室寫 10 分鐘作業
- 去前臺取下快遞
如果是在單線程情況下,JVM、CPU指令集會對其進行優化,比如,按 1->3->2的方式執行,也是沒問題的,可以少跑?次前臺。這種叫做指令重排序.
關于指令重排序引發的線程不安全問題請查詢目錄到單例模式!!!
📕4. synchronized 關鍵字
??4.1 synchronized的特性
- 互斥
synchronized 會起到互斥效果, 某個線程執行到某個對象的 synchronized 中時, 其他線程如果也執行到同?個對象 synchronized 就會阻塞等待.
synchronized用的鎖是存在Java對象頭里的。
可以粗略理解成, 每個對象在內存中存儲的時候, 都存有?塊內存表示當前的 “鎖定” 狀態(類似于廁所的 “有人/無人”).
如果當前是 “無人” 狀態, 那么就可以使用, 使用時需要設為 “有人” 狀態.
如果當前是 “有人” 狀態, 那么其他?無法使用, 只能排隊
什么是阻塞等待呢?
針對每一把鎖, 操作系統內部都維護了一個等待隊列. 當這個鎖被某個線程占有的時候, 其他線程嘗試進行加鎖, 就加不上了, 就會阻塞等待, 一直等到之前的線程解鎖之后, 由操作系統喚醒一個新的線程,再來獲取到這個鎖.
假設有 A B C 三個線程, 線程 A 先獲取到鎖, 然后 B 嘗試獲取鎖, 然后 C 再嘗試獲取鎖, 此時 B 和 C都在阻塞隊列中排隊等待. 但是當 A 釋放鎖之后, 雖然 B 比 C 先來的, 但是 B 不一定就能獲取到鎖,而是和 C 重新競爭, 并不遵守先來后到的規則.
- 可重入
synchronized 同步塊對同?條線程來說是可重入的,不會出現自己把自己鎖死的問題;
什么是自己把自己鎖死?
//第一次加鎖,命名為鎖1
synchronized (locker){
//第二次嘗試加鎖,命名為鎖2,但是此時加鎖要等到鎖1釋放鎖synchronized (locker){count++;}
}
//鎖1釋放鎖的條件鎖2中的代碼要執行完,這就是自己把自己鎖死了//理解一下這個場景,車鑰匙在家里,家門鑰匙在車里
但Java 中的 synchronized 是 可重入鎖, 因此沒有上面的問題.
🌰舉個例子:加入我追x姑娘,此時x姑娘處于未加鎖狀態 , 我可以表白成功 , 其他人也可以表白成功 . 但是如果我表白成功了, 意味著x姑娘就處于加鎖狀態了 , 其他人在想表白是不可能成功的 , 但是我無論想在表白多少次 , x姑娘都會同意
在可重入鎖的內部, 包含了 “線程持有者” 和 “計數器” 兩個信息:
? 如果某個線程加鎖的時候, 發現鎖已經被人占用, 但是恰好占用的正是自己, 那么仍然可以繼續獲取到鎖, 并讓計數器自增.
? 解鎖的時候計數器遞減為 0 的時候, 才真正釋放鎖. (才能被別的線程獲取到)
??4.2 synchronized 使用示例
- 修飾代碼塊
public class SynchronizedDemo {private Object locker = new Object();public void method() {synchronized (locker) {}}
}
- 直接修飾普通方法
public class SynchronizedDemo {public synchronized void methond() {}
}
- 修飾靜態方法
public class SynchronizedDemo {public synchronized static void method() {}
}
📕5. volatile關鍵字
- 內存可見性
import java.util.Scanner;class Counter {public int flag = 0;
}
public class Test {public static void main(String[] args) throws InterruptedException {Counter count = new Counter();Thread t1 = new Thread(()->{while (count.flag == 0){System.out.println("it is t1 main thread");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});Scanner scanner = new Scanner(System.in);Thread t2 = new Thread(()->{System.out.println("please input a number");count.flag = scanner.nextInt();});t1.start();t2.start();t1.join();t2.join();}
}
在這個代碼中:
? 創建兩個線程 t1 和 t2
? t1 中包含?個循環, 這個循環以 flag == 0 為循環條件.
? t2 中從鍵盤讀入一個整數, 并把這個整數賦值給 flag.
? 預期當用戶輸入非 0 的值的時候, t1 線程結束
結果發現輸入任意一個數字后線程t1并沒有停止(這就是一個bug)
這是因編譯器自身優化導致的bug,當編譯器發現我們頻繁load的flag是一個值得時候,就會把flag方法工作內存上,就不再上主內存load了,但是我們突然修改flag的值,主內存修改了,但是t1線程的工作內存并沒有修改
代碼在寫入 volatile 修飾的變量的時候:
? 改變線程工作內存中volatile變量副本的值
? 將改變后的副本的值從工作內存刷新到主內存
代碼在讀取 volatile 修飾的變量的時候:
? 從主內存中讀取volatile變量的最新值到線程的工作內存中
? 從工作內存中讀取volatile變量的副本
前?我們討論JMM模型時說了, 線程直接訪問工作內存(實際是 CPU 的寄存器或者 CPU 的緩存), 速度非常快, 但是可能出現數據不一致的情況. 加上 volatile , 強制讀寫內存. 速度是慢了, 但是數據變的更準確了.
- volatile不能保證原子性
雖然volatile解決了內存可見性,但是volatile不是原子的,我們想解決原子性問題還要synchronized鎖,volatile和synchronized是兩個不同維度的問題
📕6. wait與notify
因為線程都是搶占式進行的,并沒有固定的順序,是實際開發中有時候我們希望合理的協調多個線程之間的執行先后順序.
例如一場籃球賽 : 我們要讓A球員傳球 , B球員拿到球后進行投籃
完成這個協調工作, 主要涉及到以下的方法:
? wait() / wait(long timeout): 讓當前線程進入等待狀態.
? notify() / notifyAll(): 喚醒在當前對象上等待的線程.
注意: wait, notify, notifyAll 都是 Object 類的方法
??6.1 wait()方法
wait 做的事情:
? 使當前執行代碼的線程進行等待. (把線程放到等待隊列中)
? 釋放當前的鎖
? 滿足一定條件時被喚醒, 重新嘗試獲取這個鎖.
注意 : wait 要搭配 synchronized 來使用. 脫離 synchronized 使用 wait 會直接拋出異常.
wait 結束等待的條件:
? 其他線程調用該對象的 notify 方法.
? wait 等待時間超時 (wait 方法提供?個帶有 timeout 參數的版本, 來指定等待時間).
? 其他線程調用該等待線程的 interrupted 方法, 導致 wait 拋出 InterruptedException 異常.
??6.2 notify()方法
notify 方法是喚醒等待的線程.
? 方法notify()也要在同步方法或同步塊中調用,該方法是用來通知那些可能等待該對象的對象鎖的其它線程,對其發出通知notify,并使它們重新獲取該對象的對象鎖。
? 如果有多個線程等待,則有線程調度器隨機挑選出一個呈 wait 狀態的線程。(并沒有 “先來后到”)
? 在notify()方法后,當前線程不會馬上釋放該對象鎖,要等到執行notify()方法的線程將程序執行完,也就是退出同步代碼塊之后才會釋放對象鎖。
??6.3 notifyAll()方法
notify方法只是喚醒某一個等待線程. 使用notifyAll方法可以一次喚醒所有的等待線程
notifyAll 一下全都喚醒, 需要這些線程重新競爭鎖
📕7. 單例模式
首先我們要知道 , 什么是設計模式?
設計模式好比象棋中的 “棋譜”. 紅方當頭炮, 黑方馬來跳. 針對紅方的一些走法, 黑方應招的時候有一些固定的套路. 按照套路來走局勢就不會吃虧.
軟件開發中也有很多常見的 “問題場景”. 針對這些問題場景, 大佬們總結出了一些固定的套路. 按照這個套路來實現代碼, 也不會吃虧.
單例模式能保證某個類在程序中只存在唯一一份實例, 而不會創建出多個實例.
單例模式具體的實現方式有很多. 最常見的是 “餓漢” 和 “懶漢” 兩種.
- 餓漢模式
//類加載的同時創建實例
class Singleton{private Singleton instance = new Singleton();private Singleton(){};public Singleton getInstance(){return instance;}
}
- 懶漢模式—單線程版
//類加載的時候不創建實例. 第一次使?的時候才創建實例
class Singleton{private static Singleton instence = null;private Singleton(){};public static Singleton getInstance(){if (instence == null){return new Singleton();}return instence;}
}
- 懶漢模式—多線程版???
//使?雙重 if 判定, 降低鎖競爭的頻率.
//給 instance 加上了 volatile.
class Singleton{private static Object locker = new Object();private static volatile Singleton instence = null;private Singleton(){};public static Singleton getInstance(){if (instence==null){synchronized (locker){if (instence == null){return new Singleton();}}}return instence;}
}
理解雙重 if 判定:
加鎖 / 解鎖是一件開銷比較高的事情. 而懶漢模式的線程不安全只是發生在首次創建實例的時候. 因此后續使用的時候, 不必再進行加鎖了. 外層的 if 就是判定下看當前是否已經把 instance 實例創建出來了.
當多線程首次調? getInstance, 大家可能都發現 instance 為 null, 于是又繼續往下執行來競爭鎖, 其中競爭成功的線程, 再完成創建實例的操作.當這個實例創建完了之后, 其他競爭到鎖的線程就被里層 if 擋住了. 也就不會繼續創建其他實例.
📕8. 阻塞隊列
什么是阻塞隊列?
阻塞隊列是一種特殊的隊列. 也遵守 “先進先出” 的原則.
阻塞隊列能是一種線程安全的數據結構, 并且具有以下特性:
? 當隊列滿的時候, 繼續入隊列就會阻塞, 直到有其他線程從隊列中取走元素.
? 當隊列空的時候, 繼續出隊列也會阻塞, 直到有其他線程往隊列中插入元素.
阻塞隊列的?個典型應用場景就是 “生產者消費者模型”. 這是一種非常典型的開發模型.
??8.1 生產者消費者模型
生產者消費者模式就是通一個容器來解決生產者和消費者的強耦合問題。
生產者和消費者彼此之間不直接通訊,而通過阻塞隊列來進行通訊,所以生產者生產完數據之后不用等待消費者處理,直接扔給阻塞隊列,消費者不找生產者要數據,而是直接從阻塞隊列里取.
- 阻塞隊列就相當于一個緩沖區,平衡了生產者和消費者的處理能力. (削峰填谷)
- 阻塞隊列也能使生產者和消費者之間 解耦.
??8.2 標準庫中的阻塞隊列
在 Java 標準庫中內置了阻塞隊列. 如果我們需要在一些程序中使用阻塞隊列, 直接使用標準庫中的即可
-
BlockingQueue 是一個接口. 真正實現的類是 LinkedBlockingQueue.
-
put 方法用于阻塞式的入隊列, take 用于阻塞式的出隊列.
-
BlockingQueue 也有 offer, poll, peek 等方法, 但是這些方法不帶有阻塞特性.
生產者消費者模型:
import java.util.Random;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;public class Test {public static void main(String[] args) {BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<>();Thread producer = new Thread(()->{Random random = new Random();while (true){try {int value = random.nextInt(1000);blockingQueue.put(value);System.out.println("生產了:"+value);Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();}}});Thread consumer = new Thread(()->{while (true){try {int value = blockingQueue.take();System.out.println("消費了:"+value);Thread.sleep(6000);} catch (InterruptedException e) {e.printStackTrace();}}});producer.start();consumer.start();}
}
??8.3 阻塞隊列實現
? 通過 “循環隊列” 的方式來實現.
? 使用 synchronized 進行加鎖控制.
? put 插入元素的時候, 判定如果隊列滿了, 就進行 wait. (注意, 要在循環中進行 wait. 被喚醒時不一定隊列就不滿了, 因為同時可能是喚醒了多個線程).
? take 取出元素的時候, 判定如果隊列為空, 就進行 wait. (也是循環 wait)
public class BlockingQueue {private int[] items = new int[1000];private volatile int size = 0;private volatile int head = 0;private volatile int tail = 0;public void put(int value) throws InterruptedException {synchronized (this) {// 此處最好使? while.// 否則 notifyAll 的時候, 該線程從 wait 中被喚醒,// 但是緊接著并未搶占到鎖. 當鎖被搶占的時候, 可能?已經隊列滿了就只能繼續等待while (size == items.length) {wait();}items[tail] = value;tail = (tail + 1) % items.length;size++;notifyAll();}}public int take() throws InterruptedException {int ret = 0;synchronized (this) {while (size == 0) {wait();}ret = items[head];head = (head + 1) % items.length;size--;notifyAll();}return ret;}public synchronized int size() {return size;}
}
📕9. 定時器
定時器也是軟件開發中的一個重要組件. 類似于一個 “鬧鐘”. 達到一個設定的時間之后, 就執行某個指定好的代碼.
比如網絡通信中, 如果對方 500ms 內沒有返回數據, 則斷開連接嘗試重連.
比如?個 Map, 希望??的某個 key 在 3s 之后過期(自動刪除).
類似于這樣的場景就需要用到定時器.
標準庫中的定時器:
標準庫中提供了一個 Timer 類. Timer 類的核心方法為 schedule , schedule 包含兩個參數. 第一個參數指定即將要執行的任務代碼, 第二個參數指定多長時間之后執行 (單位為毫秒).
Timer timer = new Timer();timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("hello");}
}, 3000);
📕10.線程池
雖然創建銷毀線程比創建銷毀進程更輕量, 但是在頻繁創建銷毀線程的時候還是會比較低效.線程池就是為了解決這個問題. 如果某個線程不再使用了, 并不是真正把線程釋放, 而是放到?個 “池子” 中, 下次如果需要用到線程就直接從池子中取, 不必通過系統來創建了.
??10.1 ExecutorService 和 Executors
ExecutorService 表示一個線程池實例.
Executors 是一個工廠類, 能夠創建出幾種不同風格的線程池.
ExecutorService 的 submit 方法能夠向線程池中提交若干個任務.
ExecutorService service = Executors.newFixedThreadPool(1);service.submit(()->{System.out.println("this is a service");});
Executors 創建線程池的幾種方式:
newFixedThreadPool: 創建固定線程數的線程池
newCachedThreadPool: 創建線程數目動態增長的線程池.
newSingleThreadExecutor: 創建只包含單個線程的線程池
newScheduledThreadPool: 設定延遲時間后執行命令,或者定期執行命令. 是進階版的 Timer.
Executors 本質上是 ThreadPoolExecutor 類的封裝
??10.2 ThreadPoolExecutor
ThreadPoolExecutor 提供了更多的可選參數, 可以進一步細化線程池行為的設定.
把創建一個線程池想象成開個公司. 每個員工相當于一個線程.
corePoolSize: 正式員工的數量. (正式員工, 一旦錄用, 永不辭退)
maximumPoolSize: 正式員工 + 臨時工的數目. (臨時工: 一段時間不干活, 就被辭退).
keepAliveTime: 臨時工允許的空閑時間.
unit: keepaliveTime 的時間單位, 是秒, 分鐘, 還是其他值.
workQueue: 傳遞任務的阻塞隊列
threadFactory: 創建線程的工廠, 參與具體的創建線程?作
RejectedExecutionHandler: 拒絕策略, 如果任務量超出公司的負荷了接下來怎么處理.
? AbortPolicy(): 超過負荷, 直接拋出異常.
? CallerRunsPolicy(): 調用者負責處理
? DiscardOldestPolicy(): 丟棄隊列中最老的任務.
? DiscardPolicy(): 丟棄新來的任務.