導航:
【Java筆記+踩坑匯總】Java基礎+JavaWeb+SSM+SpringBoot+SpringCloud+瑞吉外賣/谷粒商城/學成在線+設計模式+面試題匯總+性能調優/架構設計+源碼解析
推薦視頻:
黑馬程序員全套Java教程_嗶哩嗶哩
尚硅谷Java入門視頻教程_嗶哩嗶哩
推薦書籍:
《Java編程思想 (第4版)》?
《Java核心技術·卷I(原書第12版) : 開發基礎》
目錄
十一、多線程
11.1 基本介紹
11.1.1? 線程和進程的關系
11.1.2 多線程
11.2 創建線程方法
11.2.0 簡介
11.2.1 方法1:繼承Thread類
11.2.2 方法2:實現 Runnable 接口?
11.2.3?方法3:實現Callable接口
11.2.4 方法4:線程池
11.3 知識加油站
11.3.1 線程生命周期
11.3.2 線程的通信方式
11.3.3 線程池
11.3.3.1 作用
11.3.3.2 生命周期?
11.3.3.3 創建線程池的方式1:線程池工具類
11.3.3.4 創建線程池的方式2:自定義線程池(推薦)
11.3.3.5 如何為線程池設置合適的線程數
11.3.3.6 線程池的原理
11.3.4 練習:多線程交替打印A/B/C,每個打印3次
11.4 線程安全
11.4.1 基本介紹?
11.4.2?原子類
11.4.3?volatile關鍵字
11.4.4?鎖
11.4.5?線程安全的集合
11.5? 線程同步
11.5.1 基本介紹
11.5.2?synchronized鎖
11.5.2.1 基本介紹
11.5.2.2 同步代碼塊
11.5.2.3 同步方法
11.5.2.4 知識加油站:synchronized鎖的原理
11.5.3 Lock鎖
11.5.4 synchronized和Lock的區別
十二、 反射
12.1 基本介紹
12.2 反射獲取Class對象
12.2.1 基本介紹
12.2.2 全限定名和規范名
12.3?反射獲取成員
12.3.1 反射獲取構造方法
12.3.2 反射獲取字段
12.3.3 反射獲取普通方法
十一、多線程
11.1 基本介紹
11.1.1? 線程和進程的關系
進程:是操作系統分配資源的基本單位,有獨立的地址空間(內存空間的一部分,用于存儲進程中的代碼、數據和堆棧等信息)和內存空間,進程之間不能共享資源,上下文切換慢,并發低,能獨立執行(有程序入口、執行序列、出口),更健壯(因為進程崩潰后不會影響其他進程)。
線程:是操作系統調度的基本單位,沒有獨立的地址空間和內存空間(只有自己的堆棧和局部變量,只能共享所在進程的內存空間),線程之間可以共享進程內的資源,上下文切換快,并發高,不能獨立執行(應用程序控制多線程執行,進程通過管理線程優先級間接控制線程執行),不健壯(因為一個線程崩潰會導致整個進程崩潰)。
關系:一個程序運行后至少包括一個進程,一個進程至少有一個線程。?
運行時數據區包括本地方法棧、虛擬機棧、方法區、堆、程序計數器。每個線程都有獨自的本地方法棧、虛擬機棧、程序計數器。各線程共享進程的方法區和堆。
JVM運行時數據區參考:
什么是JVM的內存模型?詳細闡述Java中局部變量、常量、類名等信息在JVM中的存儲位置_jvm中主要用于存儲類的元數據(類型信息(類的描述信息 類的元數據))、靜態變量、常-CSDN博客
11.1.2 多線程
一個程序運行后至少包括一個進程,一個進程至少有一個線程,一個進程下有多個線程并發地處理任務,稱為多線程。
多線程的好處:當一個線程進入阻塞或者等待狀態時,其他的線程可以獲取CPU的執行權,提高了CPU的利用率。
多線程的缺點:
- 死鎖:多個進程或線程相互等待對方釋放所持有的資源,從而無法繼續執行的情況。若無外力作用,它們都將無法推進下去。死鎖用占用CPU、內存等系統資源,導致資源浪費,死鎖會導致程序無法正常退出,導致系統性能差。
- 上下文頻繁切換:頻繁的上下文切換可能會造成資源的浪費;
- 串行:如果因為資源的限制,多線程串行執行,可能速度會比單線程更慢。
?線程的優先級:java是搶占式調度模型,每一個 Java 線程都有一個優先級,優先級是一個整數,其取值范圍是 1 (Thread.MIN_PRIORITY ) - 10 (Thread.MAX_PRIORITY )。
默認情況下,每一個線程都會分配一個優先級 NORM_PRIORITY(5)。
注意:優先級高的線程只是獲取CPU時間片的幾率高,但并不能保證先執行。
11.2 創建線程方法
11.2.0 簡介
創建線程有4種方式:
- ?繼承Thread類:繼承Thread類,重寫run()方法;然后創建線程對象調用start()方法開啟線程。start()方法里包括了run()方法,用于開啟線程。注意如果直接調用run()方法的話,將是普通方法調用,無法起到開啟線程的效果
- 實現Runnable接口:實現Runnable接口并重寫run()方法,將實現類作為構造參數創建Thread對象。推薦,因為Java是單繼承,線程類實現接口的同時,還可以繼承其他類實現其他接口。
- 實現Callable:實現Callable<T>接口,重寫帶返回值的call()方法;將實現類對象作為構造參數創建FutureTask<T>對象;將FutureTask對象作為構造參數創建Thread對象。所以此方法可以獲取線程執行完后的返回值,而前兩種方式不能。
- ExecutorService的submit或execute方法:execute和submit都是ExecutorService接口的方法,用于線程池提交任務。所有線程池都直接或間接實現ExecutorService接口。
- execute:參數只能是Runnable,沒有返回值
- submit:參數可以是Runnable、Callable,返回值是FutureTask??
11.2.1 方法1:繼承Thread類
創建并啟動線程的步驟:?
- 創建一個繼承了 Thread類的線程類,重寫的run()方法是線程執行體。
- 創建這個類的對象。
- 調用線程對象的start()方法來啟動該線程(之后Java虛擬機會調用該線程run方法)。
run()和start()區別:
- run():封裝線程執行的代碼,直接調用相當于普通方法的調用。
- start():啟動線程,虛擬機調用該線程的run()方法。
構造方法:
- Thread(): 創建一個新的線程對象。
- Thread(String name): 創建一個新的線程對象并將其名稱設置為指定的名稱。
- Thread(Runnable target): 創建一個新的線程對象并將其目標設置為指定的 Runnable 對象。主要用于后面通過Runable接口創建線程。
- Thread(Runnable target, String name): 創建一個新的線程對象,將其目標設置為指定的 Runnable 對象,并將其名稱設置為指定的名稱。
常用方法:
- void start(): 使線程開始執行;Java 虛擬機調用此線程的 run 方法。
- void run(): 如果此線程是使用獨立的 Runnable 運行對象構造的,則調用該 Runnable 對象的 run 方法;否則,此方法不執行任何操作并返回。
- void join():等待該線程執行完成。A線程調用B線程的join()方法,A線程將被阻塞,直到B線程執行完。可以用于線程之間的通信。
- void join(long millis): 等待該線程終止的時間最長為 millis 毫秒。
- void join(long millis, int nanos): 等待該線程終止的時間最長為 millis 毫秒 + nanos 納秒。
- void interrupt(): 中斷該線程。
- boolean isInterrupted(): 測試當前線程是否已中斷。
- boolean isAlive(): 測試線程是否處于活動狀態。
- static void sleep(long millis): 使當前正在執行的線程休眠(暫停執行)指定的毫秒數。
- static void sleep(long millis, int nanos): 使當前正在執行的線程休眠(暫停執行)指定的毫秒數加指定的納秒數。
屬性方法:
- void setName(String name): 改變線程名稱,使之與參數 name 相同。
- String getName(): 返回該線程的名稱。
- void setPriority(int newPriority): 更改該線程的優先級。
- int getPriority(): 返回該線程的優先級。
- Thread.State getState(): 返回該線程的狀態。
- void setDaemon(boolean on): 將該線程標記為守護線程或用戶線程。
- boolean isDaemon(): 測試該線程是否為守護線程。用戶線程是普通的線程,它們通常是應用程序執行任務的主要線程。守護線程為其他線程提供后臺支持。當所有用戶線程結束時,JVM 會自動退出,無論守護線程是否仍在運行。
代碼示例1:主線程設置名字并查看:
public static void main(String[] args) {Thread.currentThread().setName("主線程");System.out.println(Thread.currentThread().getName());}
代碼示例2:創建并啟動線程
線程類:?
/*** @Author: vince* @CreateTime: 2024/07/16* @Description: 打印數字線程類* @Version: 1.0*/ public class PrintNumberThread extends Thread{/*** 打印1-100*/@Overridepublic void run(){for(int i=0;i<100;i++) {System.out.println(getName()+":"+i);}}/*** 構造方法* @param name 線程名*/public PrintNumberThread(String name) {super(name);} }
創建并啟動線程 :
public class Test {public static void main(String[] args) {PrintNumberThread a = new PrintNumberThread ("a"), b = new PrintNumberThread ("b");a.start();b.start();} }
運行結果:
可以看到兩個線程是隨機交替打印的,因為它們獲取CPU的調度是隨機的:
?
11.2.2 方法2:實現 Runnable 接口?
Runnable翻譯:可運行的
步驟:?
- 定義Runnable接口的實現類,并實現該接口的run()方法,該方法將作為線程執行體。
- 創建Runnable實現類的實例,并將其作為參數來創建Thread對象,Thread對象為線程對象。
- 調用線程對象的start()方法來啟動該線程。
這種辦法更好,優點:
- 避免Java 單繼承局限性:Java是單繼承,使用這種方法,線程類實現接口的同時,還可以繼承其他類、實現其他接口。
- 邏輯和數據更好分離:通過實現 Runnable 接口的方法創建多線程更加適合同一個資源被多段業務邏輯并行處理的場景。在同一個資源被多個線程邏輯異步、并行處理的場景中,通過實現 Runnable 接口的方式設計多個 target 執行目標類可以更加方便、清晰地將執行邏輯和數據存儲分離,更好地體現了面向對象的設計思想。
示例:
/*** @Author: vince* @CreateTime: 2024/07/16* @Description: 打印數字Runnable* @Version: 1.0*/ public class PrintNumberRunnable implements Runnable{@Overridepublic void run(){for(int i=0;i<100;i++){System.out.println(Thread.currentThread().getName()+":"+i);}} }
public class Test {public static void main(String[] args) {// 方法1:使用普通方式實現Runnable接口PrintNumberRunnable runnable = new PrintNumberRunnable();Thread a = new Thread(runnable, "a"), b = new Thread(runnable, "b");// 方法2:使用Lambda表達式實現Runnable接口,無需再創建PrintNumberRunnable類Thread d = new Thread(() -> {for (int i = 0; i < 100; i++) {System.out.println(Thread.currentThread().getName() + ":" + i);}},"d");a.start();b.start();d.start();} }
?運行結果:?
?
?
11.2.3?方法3:實現Callable接口
通過實現Callable接口來創建線程的步驟如下
- 實現Callable<T>接口,重寫帶返回值的call()方法;
- 將實現類對象作為構造參數創建FutureTask<T>對象;
- 將FutureTask對象作為構造參數創建Thread對象。
相比于前兩種方法,此方法可以獲取線程執行完后的返回值,而前兩種方式不能,因為call()方法是有返回值的。?
代碼示例:
class MyCallable implements Callable {@Overridepublic Object call() throws Exception {return null;}
}
FutureTask task = new FutureTask(new MyCallable());new Thread(task).start();
11.2.4 方法4:線程池
線程池(Thread Pool)是一種多線程處理方式,用于減少創建和銷毀線程的開銷,提高系統資源利用率和處理效率。
線程池作用:?
- 管理線程數量:它可以管理線程的數量,可以避免無節制的創建線程,導致超出系統負荷直至崩潰。
- 讓線程復用:它還可以讓線程復用,可以大大地減少創建和銷毀線程所帶來的開銷。
線程池的兩種創建方法:
- 執行器工具類Executors;
- 自定義線程池ThreadPoolExecutor?
線程池兩種提交任務的方法:
execute和submit都是ExecutorService接口的方法,用于線程池提交任務。所有線程池都直接或間接實現ExecutorService接口。
- execute:參數只能是Runnable,沒有返回值
- submit:參數可以是Runnable、Callable,返回值是FutureTask?
代碼示例:
兩種創建線程池的方法:
線程池工具類,創建固定大小的線程池:
ExecutorService executorService = Executors.newFixedThreadPool(10);executorService.execute(new Runnable() {@Overridepublic void run() {System.out.println("當前線程"+Thread.currentThread());}});
自定義線程池(腿姐):
ThreadPoolExecutor executor = new ThreadPoolExecutor( 5, //核心線程數200, //最大線程數量,控制資源并發10, //存活時間TimeUnit.SECONDS, //時間單位new LinkedBlockingDeque<>( 100000), //任務隊列,大小100000個Executors.defaultThreadFactory(), //線程的創建工廠new ThreadPoolExecutor.AbortPolicy()); //拒絕策略// 任務1executor.execute(() -> {try {Thread.sleep(3 * 1000);System.out.println("--helloWorld_001--" + Thread.currentThread().getName());} catch (InterruptedException e) {e.printStackTrace();}});
11.3 知識加油站
11.3.1 線程生命周期
Java線程在運行的生命周期中,在任意給定的時刻,只能處于下列6種狀態之一:
- NEW :初始狀態,線程被創建,但是還沒有調用start方法。
- RUNNABLE:可運行狀態,等待調度或運行。線程正在JVM中執行,但是有可能在等待操作系統的調度。
- BLOCKED :阻塞狀態,線程正在等待獲取監視器鎖。
- WAITING :等待狀態,線程正在等待其他線程的通知或中斷。線程等待狀態不占用 CPU 資源,被喚醒后進入可運行狀態(等待調度或運行)。
- TIMED_WAITING:超時等待狀態,在WAITING的基礎上增加了超時時間,即超出時間自動返回。Thread.sleep(1000);讓線程超時等待1s。
- TERMINATED:終止狀態,線程已經執行完畢。
線程的運行過程:
線程在創建之后默認為NEW(初始狀態),在調用start方法之后進入RUNNABLE(可運行狀態)。
注意:
可運行狀態不代表線程正在運行,它有可能正在等待操作系統的調度。
WAITING (等待狀態)的線程需要其他線程的通知才能返回到可運行狀態,而TIMED_WAITING(超時等待狀態)相當于在等待狀態的基礎上增加了超時限制,除了他線程的喚醒,在超時時間到達時也會返回運行狀態。
此外,線程在執行同步方法時,在沒有獲取到鎖的情況下,會進入到BLOCKED(阻塞狀態)。線程在執行完run方法之后,會進入到TERMINATED(終止狀態)。
等待狀態如何被喚醒?
Object類:
- wait()方法讓線程進入等待狀態
- notify()喚醒該對象上的隨機一個線程
- notifyAll()喚醒該對象上的所有線程。
這3個方法必須處于synchronized代碼塊或方法中,否則會拋出IllegalMonitorStateException異常。因為調用這三個方法之前必須拿要到當前鎖對象的監視器(Monitor對象),synchronized基于對象頭和Monitor對象。
另外,也可以通過Condition類的 await/signal/signalAll方法實現線程的等待和喚醒,從而實現線程的通信,令線程之間協作處理任務。這兩個方法依賴于Lock對象。
11.3.2 線程的通信方式
線程通信:用于多個線程之間協作工作,共同完成某個任務。多個線程在并發執行的時候,他們在CPU中是隨機切換執行的,這個時候我們想多個線程一起來完成一件任務,這個時候我們就需要線程之間的通信了,多個線程一起來完成一個任務。
線程通信方式:?
- 通過 volatile 關鍵字:多個線程同時監聽一個volatile變量,當這個變量發生變化的時候 ,線程能夠感知并執行相應的業務。利用了volatile可見性,即一旦修改變量則立即刷新到共享內存中。
- 通過Object類的 wait/notify/notifyAll 方法:當我們使用synchronized同步時就會使用Monitor來實現線程通信,這里的Monitor其實就是鎖對象,其利用Object類的wait,notify,notifyAll等方法來實現線程通信。Monitor是Java虛擬機實現鎖的一種底層機制,用于控制線程對共享資源的訪問。(Object類的wait()方法讓線程進入等待狀態,notify()喚醒該對象上的隨機一個線程,notifyAll()喚醒該對象上的所有線程。這3個方法必須處于synchronized代碼塊或方法中,否則會拋出IllegalMonitorStateException異常。因為調用這三個方法之前必須拿要到當前鎖對象的監視器(Monitor對象),synchronized基于對象頭和Monitor對象。)
- 通過Condition類的 await/signal 方法:而使用Lock進行同步時就是使用Condition對象來實現線程通信,Condition對象通過Lock的lock.newCondition()方法創建,使用其await,sign或signAll方法實現線程通信。?Condition 是一個與鎖 Lock 相關聯的條件對象,可以讓等待線程在某個條件被滿足時被喚醒,從而達到線程協作的目的。
- 通過Semaphore的acquire/release方法: Semaphore是一個計數信號量,用于控制同時訪問某個資源的線程數量。線程可以通過acquire()方法獲取許可,release()方法釋放許可。
- 通過Thread類的join()方法:join() 方法是等待該線程執行完成。A線程調用B線程的join()方法,A線程將被阻塞,直到B線程執行完。
應用場景:
- 線程交替打印:在多線程交替打印A/B、或者交替打印1到100時,需要在鎖中使用線程通信。如果不使用lock.notify()和lock.wait(),可能導致當前線程釋放鎖后立刻又拿回鎖(因為多線程是CPU隨機切換的),從而達不到交替打印的效果
//第一個線程,例如打印Anew Thread(() -> {while (true) {synchronized (lock) {// 1.臨界值校驗:到臨界值喚醒其他線程,防止其他線程永遠等待;// 2.打印判斷:如果需要打印,則打印、操作原子類。 如果用的當前行值原子類,則加1;如果用的總行數原子類,則減1// 4.線程通信:喚醒、等待。// 如果刪除下面兩行代碼,可能導致當前線程釋放鎖后立刻又拿到鎖了,從而達不到交替打印的效果lock.notifyAll();try-catch{lock.wait();}}}}).start();//另一個線程,例如打印B...
11.3.3 線程池
11.3.3.1 作用
為了對多線程進行統一的管理,Java引入了線程池,它通過限制并發線程的數量、將待執行的線程放入隊列、銷毀空閑線程,來控制資源消耗,使線程更合理地運行,避免系統因為創建過多線程而崩潰。
線程池作用:?
- 管理線程數量:它可以管理線程的數量,可以避免無節制的銷毀、創建線程,導致額外的性格損耗、或者線程數超出系統負荷直至崩潰。
- 提高性能:當有新的任務到來時,可以直接從線程池中取出一個空閑線程來執行任務,而不需要等待創建新線程,從而減少了響應時間。
- 讓線程復用:它還可以讓線程復用,可以大大地減少創建和銷毀線程所帶來的開銷。
- 合理的拒絕策略:線程池提供了多種拒絕策略,當線程池隊列滿了時,可以采用不同的策略進行處理,如拋出異常、丟棄任務或調用者運行等。
11.3.3.2 生命周期?
生命周期:
通常線程池的生命周期包含5個狀態,對應狀態值分別是:-1、0、1、2、3,這些狀態只能由小到大遷移,不可逆。
- RUNNING:運行。線程池處于正常狀態,可以接受新的任務,同時會按照預設的策略來處理已有任務的執行。
- SHUTDOWN:關閉。線程池處于關閉狀態,不再接受新的任務,但是會繼續執行已有任務直到執行完成。執行線程池對象的shutdown()時進入該狀態。
- STOP:停止。線程池處于關閉狀態,不再接受新的任務,同時會中斷正在執行的任務,清空線程隊列。執行shutdownNow()時進入該狀態。
- TIDYING:整理。所有任務已經執行完畢,線程池進入該狀態會開始進行一些結尾工作,比如及時清理線程池的一些資源。
- TERMINATED:終止。線程池已經完全停止,所有的狀態都已經結束了,線程池處于最終的狀態。
11.3.3.3 創建線程池的方式1:線程池工具類
執行器工具類Executors創建線程池:?底層都是return new ThreadPoolExecutor(...)。一般不使用這種方式,參數配置死了不可控。
- newCachedThreadPool:緩存線程池(無限大)。?
- 核心線程數是0,最大線程數無限大:最大線程數Integer.MAX_VALUE。線程數量可以無限擴大,所有線程都是非核心線程。
- 空閑線程存活時間60s:keepAliveTime為60S,空閑線程超過60s會被殺死。
- 同步隊列:因為最大線程數無限大,所以也用不到阻塞隊列,所以設為沒有存儲空間的SynchronousQueue同步隊列。這意味著只要有請求到來,就必須要找到一條工作線程處理他,如果當前沒有空閑的線程,那么就會再創建一條新的線程。
- newFixedThreadPool:固定大小的線程池。
- 核心線程數:所有線程都是核心線程(通過構造參數指定),最大線程數=核心線程數。
- 存活時間0s:因為所有線程都是核心線程,所以用不到存活時間,線程都會一直存活。keepAliveTime為0S。
- 鏈表阻塞隊列:超出的線程會在LinkedBlockingQueue隊列中等待。
- newScheduledThreadPool:定時任務線程池。創建一個定長線程池, 支持定時及周期性任務執行。可指定核心線程數,最大線程數。
- newSingleThreadExecutor:單線程化的線程池。核心線程數與最大線程數都只有一個,不回收。后臺從LinkedBlockingQueue隊列中獲取任務?。創建一個單線程化的線程池, 它只會用唯一的工作線程來執行任務, 保證所有任務按照指定順序(FIFO, LIFO, 優先級)執行。?
ExecutorService executorService = Executors.newFixedThreadPool(10);//源碼
FixedThredPool: new ThreadExcutor(n, n, 0L, ms, new LinkedBlockingQueue<Runable>()
SingleThreadExecutor: new ThreadExcutor(1, 1, 0L, ms, new LinkedBlockingQueue<Runable>())
CachedTheadPool: new ThreadExcutor(0, max_valuem, 60L, s, new SynchronousQueue<Runnable>());
ScheduledThreadPoolExcutor: ScheduledThreadPool, SingleThreadScheduledExecutor.
一般要搭配計數器CountDownLatch,await(時間)讓主線程等待,直到任務線程都執行完(計數器減為零),或者到達超時時間,防止無線等待。
11.3.3.4 創建線程池的方式2:自定義線程池(推薦)
線程池執行器ThreadPoolExecutor創建自定義線程池:
ThreadPoolExecutor threadPoolExecutor= new ThreadPoolExecutor( 5, //核心線程數200, //最大線程數量,控制資源并發10, //存活時間TimeUnit.SECONDS, //時間單位new LinkedBlockingDeque<>( 100000), //任務隊列,大小100000個Executors.defaultThreadFactory(), //線程的創建工廠new ThreadPoolExecutor.AbortPolicy()); //拒絕策略CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> { //開啟異步編排,有返回值return 1;}, threadPoolExecutor).thenApplyAsync(res -> { //串行化,接收參數并有返回值return res+1;}, threadPoolExecutor);Integer integer = future.get(); //獲取返回值
?七個參數:
- corePoolSize:核心線程數。創建以后,會一直存活到線程池銷毀,空閑時也不銷毀。
- maximumPoolSize:最大線程數量。阻塞隊列滿了
- keepAliveTime: 存活時間。釋放空閑時間超過“存活時間”的線程,僅留核心線程數量的線程。
- TimeUnitunit:時間單位
- workQueue: 任務隊列。如果線程數超過核心數量,就把剩余的任務放到隊列里。只要有線程空閑,就會去隊列取出新的任務執行。new LinkedBlockingDeque()隊列大小默認是Integer的最大值,內存不夠,所以建議指定隊列大小。
- SynchronousQueue是一個同步隊列,這個阻塞隊列沒有存儲空間,這意味著只要有請求到來,就必須要找到一條工作線程處理他,如果當前沒有空閑的線程,那么就會再創建一條新的線程。
- LinkedBlockingQueue是一個無界隊列,可以緩存無限多的任務。由于其無界特性,因此需要合理地處理好任務的生產速率和線程池中線程的數量,以避免內存溢出等異常問題。無限緩存,拒絕策略就能隨意了。
- ArrayBlockingQueue是一個有界(容量固定)隊列,只能緩存固定數量的任務。通過固定隊列容量,可以避免任務過多導致線程阻塞,保證線程池資源的可控性和穩定性。推薦,有界隊列能增加系統的穩定性和預警能力。可以根據需要設大一點,比如幾千,新任務丟棄后未來重新入隊。
- PriorityBlockingQueue是一個優先級隊列,能夠對任務按照優先級進行排序,當任務數量超過隊列容量時,會根據元素的Comparable或Comparator排序規則進行丟棄或拋異常。
new PriorityBlockingQueue<>((o1, o2) -> o1.length() - o2.length());
- threadFactory:線程的創建工廠。可以使用默認的線程工廠Executors.defaultThreadFactory(),也可以自定義線程工廠(實現ThreadFactory接口)
- RejectedExecutionHandler handler:拒絕策略。如果任務隊列和最大線程數量滿了,按照指定的拒絕策略執行任務。
- Abort(默認):直接拋異常(拒絕執行異常RejectedExecutionException)
- CallerRuns:直接同步調用線程run()方法,不創建線程了
- DiscardOldest:丟棄最老任務
- Discard:直接丟棄新任務
- 實現拒絕執行處理器接口(RejectedExecutionHandler),自定義拒絕策略。
11.3.3.5 如何為線程池設置合適的線程數
下面的參數只是一個預估值,適合初步設置,具體的線程數需要經過壓測確定,壓榨(更好的利用)CPU的性能。
CPU核心數為N;
核心線程數:
- CPU密集型:N+1。數量與CPU核數相近是為了不浪費CPU,并防止頻繁的上下文切換,加1是為了有線程被阻塞后還能不浪費CPU的算力。
- I/O密集型:2N,或N/(1-阻塞系數)。I/O密集型任務CPU使用率并不是很高,可以讓CPU在等待I/O操作的時去處理別的任務,充分利用CPU,所以數量就比CPU核心數高一倍。有些公司會考慮阻塞系數,阻塞系數是任務線程被阻塞的比例,一般是0.8~0.9。
- 實際開發中更適合的公式:N*((線程等待時間+線程計算時間)/線程計算時間)
最大線程數:設成核心線程數的2-4倍。數量主要由CPU和IO的密集性、處理的數據量等因素決定。
需要增加線程的情況:jstack打印線程快照,如果發現線程池中大部分線程都等待獲取任務、則說明線程夠用。如果大部分線程都處于運行狀態,可以繼續適當調高線程數量。
jstack:打印指定進程此刻的線程快照。定位線程長時間停頓的原因,例如死鎖、等待資源、阻塞。如果有死鎖會打印線程的互相占用資源情況。線程快照:該進程內每條線程正在執行的方法堆棧的集合。
11.3.3.6 線程池的原理
任務加入時判斷的順序:核心線程數 、阻塞隊列、最大線程數、拒絕策略。
線程池執原理:?
- 新加入任務,判斷corePoolSize是否到最大值;如果沒到最大值就創建核心線程執行新任務,如果到最大值就判斷是否有空閑的核心線程;
- 如果有空閑的核心線程,則空閑核心線程執行新任務,如果沒空閑的核心線程,則嘗試加入FIFO阻塞隊列;
- 若加入成功,則等待空閑核心線程將隊頭任務取出并執行,若加入失敗(例如隊列滿了),則判斷maximumPoolSize是否到最大值;
- 如果沒到最大值就創建非核心線程執行新任務,如果到了最大值就執行丟棄策略,默認丟棄新任務;
- 線程數大于corePoolSize時,空閑線程將在keepAliveTime后回收,直到線程數等于核心線程數。這些核心線程也不會被回收。
實際上線程本身沒有核心和非核心的概念,都是靠比較corePoolSize和當前線程數判斷一個線程是不是能看作核心線程。
可能某個線程之前被看作是核心線程,等它空閑了,線程池又有corePoolSize個線程在執行任務,這個線程到keepAliveTime后還是會被回收。
11.3.4 練習:多線程交替打印A/B/C,每個打印3次
?核心邏輯:創建線程,循環加鎖,執行以下邏輯:
- 臨界值判斷:到達臨界值后喚醒其他線程并結束鎖;
- 打印判斷:如果需要打印,則打印、操作原子類(只有打印后才操作原子類,否則就是不滿足條件,需要下一步的喚醒等待后,進入下一輪的循環);
- 線程通信:喚醒、等待。
坑點:
- 臨界值判斷不能放到while里:防止最后一個線程無法喚醒其他線程,從而導致死鎖(其他線程沒人喚醒了)。
- 必須用線程通信:防止當前線程釋放鎖后立刻又拿回鎖(因為多線程是CPU隨機切換的),從而達不到交替打印的效果
具體代碼(Object類的wait()和notifyAll()方案、不抽取方法):?
/*** @Author: vince* @CreateTime: 2024/05/13* @Description: 多線交替打印A/B/C* @Version: 1.0*/
public class Test2 {/*** 當前行值*/private static AtomicInteger index = new AtomicInteger(0);/*** 總打印行數*/private static final int count = 9;public static void main(String[] args) {Object lock = new Object();// 下面創建三個線程可以抽取成一個方法,這里方便理解所以拆開new Thread(() -> {// tip:這里條件沒必要index.get()<count,因為where不在鎖里。// 如果臨界值判斷加到這里,會導致最后一個線程無法喚醒其他線程,從而導致死鎖(其他線程沒人喚醒了)。while (true) {synchronized (lock) {// 1.臨界值判斷:到達臨界值后喚醒其他線程并結束鎖;if(index.get()>=count){lock.notifyAll();break;}// 2.打印判斷:如果需要打印,則打印、操作原子類if (index.get() % 3 == 0) {System.out.println("A");// 只有打印后才操作原子類,否則就是不滿足條件,需要下一步的喚醒等待后,進入下一輪的循環index.getAndIncrement();}// 3.線程通信:喚醒、等待// 3.1 喚醒其他線程:不管能不能整除,結束后都喚醒其他線程// notifyAll()喚醒該對象上的所有線程lock.notifyAll();// 3.2 當前線程等待:Object類的wait()方法讓線程進入等待狀態,直到其他線程調用notify()或notifyAll()方法喚醒try {lock.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}}}, "線程1打印A").start();new Thread(() -> {while (true) {synchronized (lock) {if(index.get()>=count){lock.notifyAll();break;}if (index.get() % 3 == 1) {System.out.println("B");index.getAndIncrement();}lock.notifyAll();try {lock.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}}}, "線程2打印B").start();new Thread(() -> {while (true) {synchronized (lock) {if(index.get()>=count){lock.notifyAll();break;}if (index.get() % 3 == 2) {System.out.println("C");index.getAndIncrement();}lock.notifyAll();try {lock.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}}}, "線程3打印C").start();}
}
?結果:
具體代碼(創建和啟動線程抽取方法):
import java.util.concurrent.atomic.AtomicInteger;/*** @Author: vince* @CreateTime: 2024/05/13* @Description: 多線交替打印A/B/C* @Version: 1.0*/ public class Test2 {/*** 當前行值*/private static AtomicInteger index = new AtomicInteger(0);/*** 總打印行數*/private static final int count = 9;public static void main(String[] args) {Object lock = new Object();createAndStartThread("線程1打印A", lock, 0, "A");createAndStartThread("線程2打印B", lock, 1, "B");createAndStartThread("線程3打印C", lock, 2, "C");}private static void createAndStartThread(String threadName, Object lock, int remainder, String output) {new Thread(() -> {while (true) {synchronized (lock) {if (index.get() >= count) {lock.notifyAll();break;}if (index.get() % 3 == remainder) {System.out.println(output);index.getAndIncrement();}lock.notifyAll();try {lock.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}}}, threadName).start();} }
其他線程通信方式:
- Object類的wait()和notifyAll()(采用)
- Conditon的await,sign或signAll方法:創建三個Conditon對象A/B/C,A.await()就是讓A線程等待;
- Semaphore的acquire和release方法:使用三個Semaphore對象,分別初始化為1、0、0,表示A、B、C三個線程的初始許可數。每個線程在打印字母之前,需要調用對應的Semaphore對象的acquire方法,獲取許可。每個線程在打印字母之后,需要調用下一個Semaphore對象的release方法,釋放許可。
11.4 線程安全
11.4.1 基本介紹?
線程安全:程序在多線程環境下可以持續進行正確的處理,不會產生數據競爭(例如死鎖)和不一致的問題。解決方案:原子類、volatile、鎖、線程安全的集合?
線程安全的解決方案:按照資源占用情況由輕到重排列:
- 原子類:具有原子操作特征(化學中原子是最小單位、不可分割)的類,只能保證單個共享變量的線程安全
- volatile:只能保證單個共享變量的線程安全
- 鎖:可以保證臨界區內的多個共享變量線程安全。
11.4.2?原子類
原子類是具有原子操作特征(化學中原子是最小單位、不可分割)的類,原子是指一個操作是不可中斷的。即使是在多個線程一起執行的時候,一個操作一旦開始,就不會被其他線程干擾。
在java.util.concurrent.atomic包下,有一系列“Atomic”開頭的類,統稱為原子類。例如AtomicInteger替代int ,底層采用CAS原子指令實現,內部的存儲值使用volatile修飾,因此多線程之間是修改可見的。
以AtomicInteger為例,某線程調用該對象的incrementAndGet()方式自增時采用CAS嘗試修改它的值,若此時沒有其他線程操作該值便修改成功否則反復執行CAS操作直到修改成功。
CAS:不斷對變量進行原子性比較和交換,從而解決單個變量的線程安全問題。。比較內存中值和預期值,如果相等則交換,如果不相等就代表被其他線程改了則重試。?
AtomicInteger常用方法:???
- 構造方法:
- AtomicInteger?(): 創建一個初始值為0的 AtomicInteger。
- AtomicInteger(int initialValue): 創建一個初始值為 initialValue 的 AtomicInteger。
- 獲取和設置:
- int get(): 獲取當前的值。
- void set(int newValue): 設置為 newValue。
- int getAndSet(int newValue): 獲取當前值,并設置為 newValue。?
- 原子更新:
- ?boolean compareAndSet(int expect, int update): 如果當前值等于 expect,則更新為 update。
- int getAndIncrement(): 以原子方式將當前值加1,返回的是舊值。
- int incrementAndGet(): 以原子方式將當前值加1,返回的是新值。
- int getAndDecrement(): 以原子方式將當前值減1,返回的是舊值。
- int decrementAndGet(): 以原子方式將當前值減1,返回的是新值。
- int getAndAdd(int delta): 以原子方式將當前值加上 delta,返回的是舊值。
- int addAndGet(int delta): 以原子方式將當前值加上 delta,返回的是新值。
- 其他方法:??
- ?int getAndUpdate(IntUnaryOperator updateFunction): 獲取當前值,并按更新函數計算新值設置。
- int updateAndGet(IntUnaryOperator updateFunction): 按更新函數計算新值設置,并返回新值。
- int getAndAccumulate(int x, IntBinaryOperator accumulatorFunction): 獲取當前值,并按累加函數計算新值設置。
- int accumulateAndGet(int x, IntBinaryOperator accumulatorFunction): 按累加函數計算新值設置,并返回新值。?
驗證原子類的線程安全:
/*** @Author: vince* @CreateTime: 2024/06/27* @Description: 測試類* @Version: 1.0*/public class Test {public static int num=0;public static void main(String[] args) throws InterruptedException {AtomicInteger atomicInteger = new AtomicInteger(0);// 創建10個線程,分別對atomicInteger進行操作for (int i = 0; i < 10; i++) {new Thread(() -> {for (int j = 0; j < 10000; j++) {atomicInteger.incrementAndGet();num++;}}).start();}// 阻塞主線程1s,保證10個線程執行完畢Thread.sleep(1000);System.out.println(atomicInteger);System.out.println(num);} }
可以看到原子類正常加到100000,而num沒有:
?
?
11.4.3?volatile關鍵字
volatile是一個關鍵字,被volatile聲明的變量存在共享內存中,所有線程要讀取、修改這個變量,都是從內存中讀取、修改,并且修改操作是原子性的,所以它能保證線程安全。
volatile特性:
- 有序性:被volatile聲明的變量之前的代碼一定會比它先執行,而之后的代碼一定會比它慢執行。底層是在生成字節碼文件時,在指令序列中插入內存屏障防止指令重排序。
- 可見性:一旦修改變量則立即刷新到共享內存中,當其他線程要讀取這個變量的時候,最終會去內存中讀取,而不是從自己的工作空間中讀取。每個線程自己的工作空間用于存放堆棧(存方法的參數和返回地址)和局部變量。
- 原子性:volatile變量不能保證完全的原子性,只能保證單次的讀/寫操作具有原子性(在同一時刻只能被一個線程訪問和修改),自增減、復合操作(+=,/=等)則不具有原子性。這也是和synchronized的區別。
讀寫內存語義:
- 寫內存語義:當寫一個volatile變量時,JMM(Java內存模型)會把該線程本地內存中的共享變量的值刷新到主內存中。
- 讀內存語義:當讀一個volatile變量時,JMM會把該線程本地內存置為無效,使其從主內存中讀取共享變量。
有序性實現機制:
volatile有序性是通過內存屏障來實現的。內存屏障就是在編譯器生成字節碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序。
機器指令:JVM包括類加載子系統、運行時數據區、執行引擎。 執行引擎負責將字節碼指令轉為操作系統能識別的本地機器指令。
指令重排序:處理器為了提高運算速度會對指令重排序,重排序分三種類型:編譯器優化重排序、處理器指令級并行重排序、內存系統重排序。?
- 編譯器優化的重排序:編譯器在不改變單線程程序的語義前提下,可以重新安排語句的執行順序。
- 指令級并行的重排序:現在處理器采用了指令集并行技術,將多條指令重疊執行。如果不存在依賴性,處理器可以改變語句對應的機器指令的執行順序。
- 內存系統的重排序:由于處理器使用緩存和讀寫緩沖區,這使得加載和存儲操作看上去可能是在亂序執行。
11.4.4?鎖
加鎖的方式有兩種,分別是synchronized關鍵字和Lock接口(在JUC包下)。
synchronized鎖是互斥鎖,可以作用于實例方法、靜態方法、代碼塊,能夠保證同一個時刻只有一個線程執行該段代碼,保證線程安全。 在執行完或者出現異常時自動釋放鎖。synchronized鎖基于對象頭和Monitor對象,在1.6之后引入輕量級鎖、偏向鎖等優化。??
lock鎖接口可以通過lock、unlock方法鎖住一段代碼,Lock實現類都是基于AQS實現的。Lock可以讓等待鎖的線程響應中斷,而synchronized卻不行,使用synchronized時,等待的線程會一直等待下去,不能夠響應中斷。
11.4.5?線程安全的集合
- Collections工具類:Collections工具類的synchronizedXxx()方法,將ArrayList等集合類包裝成線程安全的集合類。
- 古老api:java.util包下性能差的古老api,如Vector、Hashtable
- 降低鎖粒度的并發容器:JUC包下Concurrent開頭的、以降低鎖粒度來提高并發性能的容器,如ConcurrentHashMap。
- 復制技術實現的并發容器:JUC包下以CopyOnWrite開頭的、采用寫時復制技術實現的并發容器,如CopyOnWriteArrayList。?
11.5? 線程同步
11.5.1 基本介紹
多條語句共享數據時,多線程程序會出現數據安全問題。
線程同步:即當有一個線程在對內存進行操作時,其他線程都不可以對這個內存地址進行操作,直到該線程完成操作, 其他線程才能對該內存地址進行操作,而其他線程又處于等待狀態。
Java主要通過加鎖的方式實現線程同步,而鎖有兩類,分別是synchronized關鍵字和Lock接口(在JUC包下)。
synchronized鎖是互斥鎖,可以作用于實例方法、靜態方法、代碼塊,能夠保證同一個時刻只有一個線程執行該段代碼,保證線程安全。 在執行完或者出現異常時自動釋放鎖。synchronized鎖基于對象頭和Monitor對象,在1.6之后引入輕量級鎖、偏向鎖等優化。??
lock鎖接口可以通過lock、unlock方法鎖住一段代碼,Lock實現類都是基于AQS實現的。Lock可以讓等待鎖的線程響應中斷,而synchronized卻不行,使用synchronized時,等待的線程會一直等待下去,不能夠響應中斷。
對比線程安全和線程同步:線程同步是實現線程安全的一種手段
- 線程安全:程序在多線程環境下可以持續進行正確的處理,不會產生數據競爭(例如死鎖)和不一致的問題。解決方案:原子類、volatile、鎖、線程安全的集合
- 線程同步:確保多個線程正確、有序地訪問共享資源。解決方案:鎖
11.5.2?synchronized鎖
11.5.2.1 基本介紹
synchronized鎖:?
synchronized鎖是互斥鎖,可以作用于實例方法、靜態方法、代碼塊,能夠保證同一個時刻只有一個線程執行該段代碼,保證線程安全。 在執行完或者出現異常時自動釋放鎖。synchronized鎖基于對象頭、CAS、Monitor對象,在1.6之后引入輕量級鎖、偏向鎖等優化。???
作用于三個位置:
?1.作用在靜態方法上,則鎖是當前類的Class對象。
?2. 作用在普通方法上,則鎖是當前的實例(this)。
?3. 作用在代碼塊上,則需要在關鍵字后面的小括號里,顯式指定鎖對象,例如this、Xxx.class。
11.5.2.2 同步代碼塊
同步代碼塊作用在代碼塊上,則需要在關鍵字后面的小括號里,顯式指定鎖對象,例如this、Xxx.class。
同步代碼塊簡單來說就是將一段代碼用一把鎖給鎖起來, 只有獲得了這把鎖的線程才訪問, 并且同一時刻, 只有一個線程能持有這把鎖, 這樣就保證了同一時刻只有一個線程能執行被鎖住的代碼.
? ? synchronized(同步對象) {//多條語句操作共享數據的代碼?}
?
同步代碼塊的好處:解決了多線程的數據安全問題
弊端:線程很多時,每個線程都會去判斷鎖,這是很耗費資源和時間的。
代碼示例:共有100張票,三個窗口賣票,通過加鎖防止超賣
public class SellTicket implements Runnable {private int tickets = 100;private final Object obj = new Object();@Overridepublic void run() {while (true) {synchronized (obj) {if (tickets > 0) {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " 正在出售第 " + tickets + " 張票");tickets--;} else {break;}}}}public static void main(String[] args) {SellTicket sellTicket = new SellTicket();Thread t1 = new Thread(sellTicket, "窗口1");Thread t2 = new Thread(sellTicket, "窗口2");Thread t3 = new Thread(sellTicket, "窗口3");t1.start();t2.start();t3.start();} }
11.5.2.3 同步方法
?1.作用在靜態方法上,則鎖是當前類的Class對象。
?2. 作用在普通方法上,則鎖是當前的實例(this)。
非靜態同步方法的鎖對象為this。下面代碼是相同功能的同步方法和同步代碼塊:
代碼示例:
鎖的粒度是當前對象:?
// 方法1:實例方法,使用this對象鎖private void sellTicket1() {synchronized (this) {if (tickets > 0) {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " 正在出售第 " + tickets + " 張票");tickets--;}}}// 方法2:實例方法,使用this對象鎖private void sellTicket2() {synchronized (this) {if (tickets > 0) {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " 正在出售第 " + tickets + " 張票");tickets--;}}}
鎖的粒度是真個類:
靜態同步方法的鎖對象為:類名.class。下面代碼是相同功能的同步方法和同步代碼塊?
// 方法3:靜態方法,使用類對象鎖private static synchronized void sellTicket3() {if (tickets > 0) {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " 正在出售第 " + tickets + " 張票");tickets--;}}// 方法4:靜態方法,使用類對象鎖private static void sellTicket4() {synchronized (SellTicket.class) {if (tickets > 0) {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " 正在出售第 " + tickets + " 張票");tickets--;}}}
11.5.2.4 知識加油站:synchronized鎖的原理
synchronized鎖:?
synchronized鎖是互斥鎖,可以作用于實例方法、靜態方法、代碼塊,能夠保證同一個時刻只有一個線程執行該段代碼,保證線程安全。 在執行完或者出現異常時自動釋放鎖。synchronized鎖基于對象頭、CAS、Monitor對象,在1.6之后引入輕量級鎖、偏向鎖等優化。???
作用于三個位置:
?1.作用在靜態方法上,則鎖是當前類的Class對象。
?2. 作用在普通方法上,則鎖是當前的實例(this)。
?3. 作用在代碼塊上,則需要在關鍵字后面的小括號里,顯式指定鎖對象,例如this、Xxx.class。
對象頭存儲鎖信息: synchronized的底層是采用對象頭的Mark Word來存儲鎖信息的。Hotspot 虛擬機(JVM默認虛擬機)中每個對象都有一個對象頭(Object Header),包含Mark Word(標記字段) 和 Class Pointer(類型指針)。
- Mark Word(標記字段):存儲哈希碼、GC分代年齡、鎖信息、GC標記(標志位,標記可達對象或垃圾對象)等。鎖信息包括:
- 鎖標志位:64位的二進制數,通過末尾能判斷鎖狀態。01未鎖定、01可偏向、00輕量級鎖、10重量級鎖、11垃圾回收標記
- 偏向鎖線程ID、時間戳等;
- 輕量級鎖的指針:指向鎖記錄的指針
- 重量級鎖的指針:指向Monitor鎖的指針
- 類型指針:指向它的類元數據的指針,用于找到對象所在的類。??
不考慮共享資源是類變量等特殊情況的話,有共享資源的多個線程通常都屬于同一個對象。??
Monitor對象:每個 Java 對象都可以關聯一個 Monitor 對象,也稱為監視器鎖或Monitor鎖。Monitor鎖用于控制線程對共享資源的訪問,開發人員不能直接訪問Monitor對象。當一個線程獲取了Monitor的鎖后,其他試圖獲取該鎖的線程就會被阻塞,直到當前線程釋放鎖為止。
當一個線程執行synchronized方法或代碼塊并升級成重量級鎖時,當前對象會關聯一個Monitor對象,線程必須先獲得該對象的Monitor鎖才能執行。Monitor有Owner、EntryList、WaitSet三個字段,分別表示Monitor的持有者線程(獲得鎖的線程)、阻塞隊列、和等待隊列。
線程通信:synchronized通過Monitor對象,利用Object的wait,notify,notifyAll等方法來實現線程通信。
鎖升級:JDK6之前synchronized只有無鎖和重量級鎖兩個狀態,JDK6引入偏向鎖、輕量級鎖兩個狀態,鎖可以根據競爭程度從無鎖狀態慢慢升級到重量級鎖。當競爭小的時候,只需以較小的代價加鎖,直到競爭加劇,才使用重量級鎖,從而減小了加鎖帶來的開銷。
- 鎖升級順序:無鎖狀態、偏向鎖狀態、輕量級鎖狀態、重量級鎖狀態。
- 無鎖:沒有線程訪問同步代碼塊時。沒有對資源進行鎖定,所有的線程都能訪問并不斷修改同一個資源,但同時只有一個線程能修改成功,失敗線程會不斷重試。
- 偏向鎖:當有一個線程訪問同步代碼塊時升級為偏向鎖。一段同步代碼塊一直被一個線程所訪問,那么該線程id會CAS寫入對象頭,下次再訪問同步代碼塊時對象頭檢查到該線程id,就不用加鎖解鎖了,降低獲取鎖的代價。
- 輕量級鎖(自旋鎖):有鎖競爭時升級為輕量級鎖。其他線程會通過自旋的形式嘗試通過CAS將對象頭中Mark Word替換為指向線程棧幀里鎖記錄的指針,從而獲得鎖;同時線程鎖記錄里存放Mark Word信息。競爭的線程不會阻塞但會一直自旋,消耗CPU性能,但速度快。
- 重量級鎖:鎖膨脹(自旋失敗10次)時升級為重量級鎖。Mark Word中存儲的是指向Monitor鎖的指針,對象Mark Word信息也會保存在Monitor鎖里,當一個線程獲取了Monitor鎖后,競爭線程會被阻塞,不再自旋,不消耗CPU,速度慢。
11.5.3 Lock鎖
Lock提供比同步方法和代碼塊更廣泛的鎖定操作。
- lock():獲取鎖。如果鎖不可用,則當前線程將被禁用,直到獲取鎖為止。
- tryLock():嘗試獲取鎖,如果鎖可用,則獲取并立即返回 true;如果鎖不可用,則立即返回 false,不會等待。
- tryLock(long time, TimeUnit unit):嘗試在指定的時間內獲取鎖。如果鎖可用,則獲取并立即返回 true;如果在指定時間內鎖不可用,則等待直到超時,然后返回 false。
- unlock():釋放鎖。
- newCondition():返回一個綁定到此 Lock 實例的新 Condition 實例,可以用于線程之間的協調等待。
代碼示例:?
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock;public class SellTicket implements Runnable {private int tickets = 100;private Lock lock = new ReentrantLock();@Overridepublic void run() {while (true) {try {lock.lock();if (tickets > 0) {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " 正在出售第 " + tickets + " 張票");tickets--;} else {break;}} finally {lock.unlock();}}}public static void main(String[] args) {SellTicket sellTicket = new SellTicket();Thread t1 = new Thread(sellTicket);Thread t2 = new Thread(sellTicket);Thread t3 = new Thread(sellTicket);t1.start();t2.start();t3.start();} }
?
11.5.4 synchronized和Lock的區別
Lock和synchronized有以下幾點不同:
- 接口和關鍵字。Lock是一個接口,而synchronized是Java中的關鍵字,synchronized是內置的語言實現;
- 死鎖問題。synchronized在發生異常時,會自動釋放線程占有的鎖,因此不會導致死鎖現象發生;而Lock在發生異常時,如果沒有主動通過unLock()去釋放鎖,則很可能造成死鎖現象,因此使用Lock時需要在finally塊中釋放鎖;
- 讓等待鎖的線程響應中斷。Lock可以可以通過lockInterruptibly()獲取鎖的方法讓等待鎖的線程響應中斷。而synchronized卻不行,使用synchronized時,等待的線程會一直等待下去,不能夠響應中斷;
- 得知是否成功獲取鎖。通過Lock可以通過tryLock()知道有沒有成功獲取鎖,而synchronized卻無法辦到。
- 性能對比。兩者性能差不多。JDK6之前synchronized沒有鎖升級,線程競爭非常激烈時Lock的性能要遠遠優于synchronized;而JDK6引入鎖升級后,線程競爭激烈時候兩者性能也相差無幾。
lock鎖中斷線程:若有線程已拿到鎖,其他線程使用lock()獲取鎖時會阻塞,使用lockInterruptibly()獲取鎖時會直接中斷拋出InterruptedException異常
lock鎖編碼習慣:加鎖代碼要放到try外面。如果放在try里面的話,加鎖失敗拋異常或者加鎖前的代碼拋異常后,執行finally里的解鎖代碼,而其實加鎖都沒成功,最終解鎖就也不合適了。
lock.lock(); // 加鎖
try{// do something
}finally{lock.unlock(); // 解鎖
}//不推薦
try{
int a=3/0;//這里拋異常會直接進入finally
lock.lock(); // 加鎖// do something
}finally{lock.unlock(); // 解鎖
}
分布式鎖:SETNX、Redisson。Redisson基于Redis協議,可以實現可重入鎖、公平鎖、讀寫鎖、信號量、閉鎖(計數器),支持看門狗自動續期。
十二、 反射
12.1 基本介紹
反射:在程序運行期間動態地獲取類的信息并對類進行操作的機制。
通過反射機制可以實現:
- 獲取類或對象的Class對象:程序運行時,可以通過反射獲得任意一個類的Class對象,并通過這個對象查看這個類的所有方法和屬性(包括私有,私有需要給該字段調用setAccessible(true)方法開啟私有權限)。注意類的class對象是運行時生成的,類的class字節碼文件是編譯時生成的。
- 創建實例:程序運行時,可以利用反射先創建類的Class對象再創建該類的實例,并訪問該實例的成員;Xxx.class.newInstance() ;例如在Spring容器類源碼里,Bean實例化就是通過Bean類的Class對象。Bean類的Class對象是從BeanDefinition對象的type成員變量取的。BeanDefinition對象存儲一些Bean的類型、名稱、作用域等聲明信息。
- 生成動態代理類或對象:程序運行時,可以通過反射機制生成一個類的動態代理類或動態代理對象。例如JDK中Proxy類的newProxyInstance靜態方法,可以通過它創建基于接口的動態代理對象。
類的字節碼文件和Class對象的區別:
- 類的class字節碼文件是編譯時生成的,類的class對象是運行時生成的。
- 類的字節碼文件是一個存儲在電腦硬盤中的文件,例如Test.class;類的Class對象是存放在內存中的數據,可以快速獲取其中的信息;
- 兩者都存儲類的各種信息;
獲取類Class對象的JVM底層:如果該類沒有被加載過,會首先通過JVM實現類的加載過程,即加載、鏈接(驗證、準備、解析)、初始化,加載階段會生成類的Class對象。
獲取類Class對象的方法:dog.getClass();Dog.class;Class.forName("package1.Dog");
特點:
- 訪問私有成員:構造方法、成員變量、方法對象取消訪問檢查可以訪問私有成員;public void setAccessible(boolean flag):值為true,取消訪問檢查
- 越過泛型檢查:反射可以越過泛型檢查,例如在ArrayList<Integer>中添加字符串
反射的優缺點:
- 優點:
- 運行時獲取屬性:運行期間能夠動態的獲取類,提高代碼的靈活性。
- 訪問私有成員:構造方法、成員變量、方法對象取消訪問檢查可以訪問私有成員;public void setAccessible(boolean flag):值為true,取消訪問檢查
- 越過泛型檢查:反射可以越過泛型檢查,例如在ArrayList<Integer>中添加字符串
- 缺點:性能差。性能比直接的Java代碼要差很多。??
應用場景:
- JDBC加載數據庫的驅動:使用JDBC時,如果要創建數據庫的連接,則需要先通過反射機制加載數據庫的驅動程序;
- Bean的生命周期:
- 實例化xml解析出的類:多數框架都支持注解或XML配置來定義應用程序中的類,從xml配置中解析出來的類是字符串,需要利用反射機制實例化;例如Spring通過<bean id="xx" class="類全限定名">和<property name="按名稱注入" ref="被注入Bean的id">定義bean,然后通過Class.forName("xxx.Xxx")獲取類的class對象,然后創建實例。
- 注解容器類加載Bean、實例化Bean:Bean的生命周期中,注解容器類的構造方法會遍歷@ComponentScan("掃描路徑")下的.class文件,通過類加載器.load("類名")方式獲得類的class對象,存入beanDefinitionMap。然后遍歷beanDefinitionMap,通過class對象實例化等。
- AOP創建動態代理對象:面向切面編程(AOP)的實現方案,是在程序運行時創建目標對象的代理對象,這必須由反射機制來實現。?
驗證反射可以繞過泛型檢查:
基于反射,我們可以給ArrayList<Integer>對象中,加入字符串
public class Test {/*** 測試方法,實際場景建議try-catch,而不是throws* @param args* @throws NoSuchMethodException* @throws InvocationTargetException* @throws IllegalAccessException*/public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {ArrayList<Integer> integers = new ArrayList<>();Class<? extends ArrayList> aClass = integers.getClass();Method add = aClass.getMethod("add", Object.class);add.invoke(integers, 1);add.invoke(integers, 2);add.invoke(integers, 3);add.invoke(integers, 4);add.invoke(integers, "hello");System.out.println(integers);} }
12.2 反射獲取Class對象
12.2.1 基本介紹
Class類的對象:程序運行時,可以通過反射獲得任意一個類的Class對象,并通過這個對象查看這個類的所有方法和屬性(包括私有,私有需要給該字段調用setAccessible(true)方法開啟私有權限)。
注意類的class對象是運行時生成的,類的class字節碼文件是編譯時生成的。
獲取類Class對象:
- 對象.getClass():Object是一切類的根類,Object類有個getClass()方法可以獲取類的Class對象。例如dog.getClass();
- 類名.class(推薦):例如Dog.class;
- Class.forName("類名"):例如Class.forName("package1.Dog");
Class對象的常用方法:
- 獲取類的信息:
- String getName():返回類的全限定名。全限定名包含包名和類名,用于唯一標識類或接口。例如package1.Dog、java.lang.String、java.util.Map$Entry
- String getSimpleName():返回類的簡單名。例如Dog
- String getCanonicalName():返回類的規范名。規范名是類的規范字符串形式,常用于打印和日志記錄。例如package1.Dog、java.lang.String、java.util.Map.Entry
- Package getPackage():返回此類所屬的包。
- ClassLoader getClassLoader():返回該類的類加載器。
- Class<? super T> getSuperclass():返回表示類的超類的 Class 對象。
- Class<?>[] getInterfaces():返回類實現的所有接口。
- boolean isInterface():判斷是否是接口。
- boolean isAnnotation():判斷是否是注解類型。
- boolean isEnum():判斷是否是枚舉類型。
- Annotation[] getAnnotations():返回此元素上存在的所有注解。
- Annotation[] getDeclaredAnnotations():返回直接存在于此元素上的所有注解。
- T getAnnotation(Class<T> annotationClass):返回指定類型的注解,如果該注解存在于此元素上,否則返回 null。例如Spring源碼中,ApplicaitonContext構造器判斷一個類是不是Bean,是通過這個方法判斷類有沒有@Comonent等注解,從而判斷它是不是Bean。
- 獲取成員:
- Field[] getFields():返回類的所有公共字段,包括從父類繼承的字段。
- Field[] getDeclaredFields():返回類聲明的所有字段,不包括繼承的字段。
- Method[] getMethods():返回類的所有公共方法,包括從父類繼承的方法。
- Method[] getDeclaredMethods():返回類聲明的所有方法,不包括繼承的方法。
- Constructor<?>[] getConstructors():返回類的所有公共構造方法。
- Constructor<?>[] getDeclaredConstructors():返回類聲明的所有構造方法。
- 其他方法:
- T newInstance():創建此 Class 對象所表示的類的一個新實例(使用默認構造方法)。
Spring源碼:Bean初始化時判斷類是否Bean、判斷屬性是否需要填充都用到了反射
Spring框架中Bean是如何加載的?從底層源碼入手,詳細解讀Bean的創建流程-CSDN博客
代碼示例:
準備Dog類
/*** @Author: vince* @CreateTime: 2024/07/02* @Description: 狗類* @Version: 1.0*/ public class Dog{/*** 體重*/private int weight;/*** 名字*/public String name;public Dog() {}public int getWeight() {return weight;}public void setWeight(int weight) {this.weight = weight;}public String getName() {return name;}public void setName(String name) {this.name = name;}public Dog(int weight, String name) {this.weight = weight;this.name = name;}@Overridepublic String toString() {return "Dog{" +"weight=" + weight +", name='" + name + '\'' +'}';} }
獲取類的Class對象:?
public class Test {/*** 測試方法,實際場景建議try-catch,而不是throws* @param args*/public static void main(String[] args) throws ClassNotFoundException {//方法1:類的class屬性Class<Dog> c1=Dog.class;//方法2:對象的getClass方法Dog wangCaiDog = new Dog(23, "旺財");Class<? extends Dog> c2= wangCaiDog.getClass();//方法3:Class類的靜態方法forNameClass<?> c3= Class.forName("package1.Dog");// 方法4:使用類加載器ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();Class<?> c5 = systemClassLoader.loadClass("package1.Dog");// 輸出:class package1.DogSystem.out.println(c1);//三種方式獲取到Class對象地址是完全一致的// 輸出:trueSystem.out.println(c1==c2&&c1==c3);} }
獲取類的信息:
/*** @Author: vince* @CreateTime: 2024/06/27* @Description: 測試類* @Version: 1.0*/public class Test {/*** 測試方法,實際場景建議try-catch,而不是throws** @param args*/public static void main(String[] args) throws Exceptionn {Class<? extends Dog> dogClass = Dog.class;System.out.println(dogClass.getName());System.out.println(dogClass.getSimpleName());System.out.println(dogClass.getCanonicalName());} }
12.2.2 全限定名和規范名
全限定名和規范名:
外部類的全限定名和規范名是一樣的,都是“xxx.類名”。區別主要在內部類,內部類的全限定名是“xxx.外部類名$內部類名”,規范名是“xxx.外部類名.內部類名”。
- 簡單名:只包含類名。例如Dog、String、Entry
- 全限定名:包含包名和類名,用于唯一標識類或接口,通過全限定名能找到唯一一個類。例如package1.Dog、java.lang.String、java.util.Map$Entry
- 規范名:類的規范字符串形式,常用于打印和日志記錄。例如package1.Dog、java.lang.String、java.util.Map.Entry
代碼示例:
public static void main(String[] args) throws Exception {// java.util.MapSystem.out.println(Map.class.getName());// java.util.MapSystem.out.println(Map.class.getCanonicalName());// 輸出 "java.util.Map$Entry"System.out.println(Map.Entry.class.getName());// 輸出 "java.util.Map.Entry"System.out.println(Map.Entry.class.getCanonicalName());}
12.3?反射獲取成員
12.3.1 反射獲取構造方法
Class對象獲取構造器:
- getConstructor(Class<?>... parameterTypes):獲取指定參數類型的公共構造方法。返回值是Constructor類。
- getDeclaredConstructor(Class<?>... parameterTypes):獲取指定參數類型的構造方法(包括私有構造方法)。
- getConstructors():獲取所有公共構造方法。
- getDeclaredConstructors():獲取所有構造方法(包括私有構造方法)。
- newInstance():創建類的新實例。
- Class類的newInstance():只能夠調用無參構造函數;
- Constructor類的newInstance():可以根據傳入的參數,調用任意構造函數。
Constructor譯作構造方法,構造器。
代碼示例:
獲取所有構造器對象:
/*** @Author: vince* @CreateTime: 2024/07/02* @Description: 狗類* @Version: 1.0*/ public class Dog{/*** 體重*/private int weight;/*** 名字*/public String name;public Dog() {}public int getWeight() {return weight;}public void setWeight(int weight) {this.weight = weight;}public String getName() {return name;}public void setName(String name) {this.name = name;}public Dog(int weight, String name) {this.weight = weight;this.name = name;}@Overridepublic String toString() {return "Dog{" +"weight=" + weight +", name='" + name + '\'' +'}';} }
public static void main(String[] args) {Class<Dog> dogClass=Dog.class;Constructor<?>[] cons = dogClass.getDeclaredConstructors();for(Constructor<?> con:cons){System.out.println(con);}}
??
?獲取單個構造器并實例化:
無參:
/*** @Author: vince* @CreateTime: 2024/06/27* @Description: 測試類* @Version: 1.0*/public class Test {/*** 測試方法,實際場景建議try-catch,而不是throws* @param args*/public static void main(String[] args) throws InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException {Class<Dog> dogClass=Dog.class;//獲取單個構造方法對象Constructor<?> con=dogClass.getDeclaredConstructor();//構造方法對象實例化,會調用無參構造方法Object dogObject = con.newInstance();// 無參構造器實例化,也可以直接用Class對象的newInstance方法,帶參就不行了Dog dog = dogClass.newInstance();//重寫了Dog類的to_String,所以輸出:Dog{weight=0, name='null'}System.out.println(dogObject);System.out.println(dog);} }
帶參:
/*** @Author: vince* @CreateTime: 2024/06/27* @Description: 測試類* @Version: 1.0*/public class Test {/*** 測試方法,實際場景建議try-catch,而不是throws* @param args*/public static void main(String[] args) throws InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException {Class<Dog> dogClass=Dog.class;Constructor<?> con=dogClass.getConstructor(int.class,String.class);Object obj = con.newInstance(32,"旺財");System.out.println(obj);} }
12.3.2 反射獲取字段
Class對象獲取字段:
- getField(String name):返回指定名稱的公共字段。返回類型是字段類Field。
- getDeclaredField(String name):返回指定名稱的字段(包括私有字段)。
- getFields():返回所有公共字段。
- getDeclaredFields():返回所有字段(包括私有字段)。
字段類Field常用方法:
獲取字段信息:
- getName():返回字段的名稱。
- getType():返回字段的類型。
- getModifiers():返回字段的修飾符。
- getDeclaringClass():返回聲明該字段的類的 Class 對象。
獲取和設置字段值:
- get(Object obj):返回指定對象上此字段的值。
- getBoolean(Object obj):返回指定對象上此字段的值(如果字段類型是 boolean)。
- getByte(Object obj):返回指定對象上此字段的值(如果字段類型是 byte)。
- getChar(Object obj):返回指定對象上此字段的值(如果字段類型是 char)。
- getDouble(Object obj):返回指定對象上此字段的值(如果字段類型是 double)。
- getFloat(Object obj):返回指定對象上此字段的值(如果字段類型是 float)。
- getInt(Object obj):返回指定對象上此字段的值(如果字段類型是 int)。
- getLong(Object obj):返回指定對象上此字段的值(如果字段類型是 long)。
- getShort(Object obj):返回指定對象上此字段的值(如果字段類型是 short)。
- set(Object obj, Object value):設置指定對象上此字段的值。注意私有字段默認不允許賦值,要賦值必須給私有字段setAccessible(true)
- setBoolean(Object obj, boolean value):設置指定對象上此字段的值(如果字段類型是 boolean)。
- setByte(Object obj, byte value):設置指定對象上此字段的值(如果字段類型是 byte)。
- setChar(Object obj, char value):設置指定對象上此字段的值(如果字段類型是 char)。
- setDouble(Object obj, double value):設置指定對象上此字段的值(如果字段類型是 double)。
- setFloat(Object obj, float value):設置指定對象上此字段的值(如果字段類型是 float)。
- setInt(Object obj, int value):設置指定對象上此字段的值(如果字段類型是 int)。
- setLong(Object obj, long value):設置指定對象上此字段的值(如果字段類型是 long)。
- setShort(Object obj, short value):設置指定對象上此字段的值(如果字段類型是 short)。
其他方法:
- isAccessible():返回字段是否可訪問。
- setAccessible(boolean flag):設置字段的可訪問性。通過這個方法可以讓私有字段也可以賦值。
- toGenericString():返回字段的描述,包括泛型信息。
- getAnnotatedType():返回此字段的帶注釋的類型。
- getAnnotations():返回字段的所有注解。
- getAnnotation(Class<T> annotationClass):返回字段的指定類型的注解,如果該注解不存在,則返回 null。例如Spring源碼中依賴注入這一塊,就是基于反射獲取類中字段有沒有@Resource、@Component等注解,有的話就是要注入Bean.
- getDeclaredAnnotations():返回直接存在于此字段上的所有注解。
Spring源碼:Bean初始化時判斷類是否Bean、判斷屬性是否需要填充都用到了反射
Spring框架中Bean是如何加載的?從底層源碼入手,詳細解讀Bean的創建流程-CSDN博客
代碼示例:
準備Dog類
/*** @Author: vince* @CreateTime: 2024/07/02* @Description: 狗類* @Version: 1.0*/ public class Dog{/*** 體重*/private int weight;/*** 名字*/public String name;public Dog() {}public int getWeight() {return weight;}public void setWeight(int weight) {this.weight = weight;}public String getName() {return name;}public void setName(String name) {this.name = name;}public Dog(int weight, String name) {this.weight = weight;this.name = name;}@Overridepublic String toString() {return "Dog{" +"weight=" + weight +", name='" + name + '\'' +'}';} }
獲取成員變量對象并賦值:?
/*** @Author: vince* @CreateTime: 2024/06/27* @Description: 測試類* @Version: 1.0*/public class Test {/*** 測試方法,實際場景建議try-catch,而不是throws** @param args*/public static void main(String[] args) throws NoSuchFieldException, InstantiationException, IllegalAccessException {// 1.獲取Class對象,并實例化Class<? extends Dog> dogClass = Dog.class;Dog dog = dogClass.newInstance();// 2.獲取字段對象Field nameField = dogClass.getDeclaredField("name");Field weightField = dogClass.getDeclaredField("weight");// 3.給字段對象賦值nameField.set(dog, "旺財");// 注意私有字段默認不允許賦值,要賦值必須給私有字段設置可訪問weightField.setAccessible(true);weightField.set(dog, 10);System.out.println(dog);} }
可以看到私有、公有字段都賦值成功:?
通過Field獲取的Class類:
/*** @Author: vince* @CreateTime: 2024/06/27* @Description: 測試類* @Version: 1.0*/public class Test {/*** 測試方法,實際場景建議try-catch,而不是throws* @param args*/public static void main(String[] args) throws NoSuchFieldException {Class<Dog> dogClass=Dog.class;Field weightField = dogClass.getDeclaredField("name");Class<?> dogClassByField = weightField.getDeclaringClass();// 通過字段獲取到的class對象和源class對象是地址是一樣的,事實上一個類的所有Class對象都是一個實例// trueSystem.out.println(dogClassByField==dogClass);} }
12.3.3 反射獲取普通方法
Class對象獲取成員方法的方法:?
- getMethod(String name, Class<?>... parameterTypes):返回指定名稱和參數類型的公共方法。返回值是方法類Method。
- getDeclaredMethod(String name, Class<?>... parameterTypes):返回指定名稱和參數類型的方法(包括私有方法)。
- getMethods():返回所有公共方法(包括從父類繼承的方法)。
- getDeclaredMethods():返回所有方法(包括私有方法)。
Method類的方法:
- 獲取方法信息:
- getName():返回方法的名稱。
- getReturnType():返回方法的返回類型。
- getParameterTypes():返回方法參數類型的數組。
- getModifiers():返回方法的修飾符。
- getDeclaringClass():返回聲明此方法的類的 Class 對象。
- 調用方法:
- Object invoke(Object obj, Object... args):調用指定對象上此 Method 對象表示的基礎方法。
- 其他方法:
- isAccessible():返回方法是否可訪問。
- setAccessible(boolean flag):設置方法的可訪問性。
- getAnnotations():返回此方法的所有注解。例如Spring源碼中通過此方法判斷一個類中
- isAnnotationPresent(Class<? extends Annotation> annotationClass):判斷此方法是否被指定的注解類型注釋。
- getAnnotation(Class<T> annotationClass):返回該方法的指定類型的注解。
- getExceptionTypes():返回此方法拋出的異常類型的數組。
- toGenericString():返回方法的描述,包括泛型信息。
獲取成員變量對象并調用:
// 1.獲取構造方法對象并實例化Class<?> c= Class.forName("train.Dog");Constructor<?> con=c.getConstructor();Object obj = con.newInstance();
// 2.獲取成員方法對象Method eat = c.getMethod("eat");
// 3.通過成員方法對象的invoke方法,調用構造方法對象的成員方法
//無參無返回值方法eat.invoke(obj);
//帶參有返回值方法Object sucess= eat.invoke(obj,"food");System.out.println((boolean)sucess);