??個人主頁
??JavaSE專欄
JAVAEE初階01
操作系統
1.對下(硬件)管理各種計算機設備
2.對上(軟件)為各種軟件提供一個穩定的運行環境
線程
運行的程序在操作系統中以進程的形式存在
進程是系統分配資源的最小單位
進程與線程的關系:
每當創建一個進程,就會包含一個線程,這個線程叫做主線程
進程相當于一個工廠,線程相當于工廠里干活的工人
進程和線程的區別和聯系
-
一個進程中至少包含一個線程,這個線程就是主線程
-
進程是申請系統資源的最小單位
-
線程是CPU調度的最小單位
-
線程共享進程申請來的所有資源
-
如果一個線程崩潰,就會影響整個進程
線程的優勢
- 線程的創建速度比進程快
- 線程的銷毀速度比進程快
- 線程的CPU調度的速度比進程快
+++
-
進程和進程之間不會相互影響,一個進程出了問題不會影響其他的進程,進程之間是相互獨立的
-
一個進程中的線程會相互影響
-
如果一個線程崩潰,就會影響整個進程
+++
并發和并行
- 并發(Concurrency)
- 定義:
多個任務在邏輯上交替執行,看起來像是“同時進行”,但實際是通過時間片輪轉(如單核 CPU 切換任務)實現的
- 并行(Parallelism)
- 定義:
多個任務真正同時執行,需要依賴多核 CPU、多處理器或分布式系統 - 關鍵點:
- 必須有多核/多處理器硬件支持
- 目標是提升計算吞吐量(如拆分任務到多個核)
- 任務是物理上的同時執行
利用Java代碼創建線程
new
一個Thread
類的對象,調用這個類的start()
方法,start()
方法會調用一個start0()
方法,觀察start0()
方法的源碼,這個start0()
方法使用了native
修飾,說明這個方法是用c/c++代碼寫的,功能是JVM通過操作系統的API
去創建一個真正的系統線程,這個線程也對應著一個PCB,然后操作系統會把這個PCB加入到一個PCB鏈表中,等待被CPU執行
線程的創建方式
- 繼承
Thread
類,重寫run
方法 - 實現
Runnable
接口,重寫run
方法
面試題:Thread類的start()方法和run()方法之間的區別?
start()
方法,真實的申請系統線程PCB,從而啟動一個線程,參與CPU調度run()
方法,定義線程時指定線程要執行的任務,如果調用了run()
方法,只是Java對象的一個普通方法而已
++++
什么是函數式接口?
接口中只有一個方法的接口
函數式接口是指只有一個抽象方法,但接口中也可以存在默認方法(default
修飾)和靜態方法、object
類的方法,如果一個接口中覆蓋了Object
類中的方法 例如(equals
方法),那么這個覆蓋的方法不視為抽象方法
+++
Thread
是java中的類
創建的Thread
對象 —> 調用start方法 —> JVM調用系統API生成一個PCB —> PCB與java對象一一對應
進程分為前臺進程和后臺進程
如果一個線程是前臺進程,那么他不受
main
方法的影響,main
方法結束后,該前臺進程依舊執行如果一個線程是后臺進程,那么他會受到main方法的影響,
main
方法結束后,該后臺進程也會自動結束
-
設置成后臺進程之后,
main
方法執行完成之后,整個程序就退出了,子線程也就自動結束了 -
前臺進程不受
main
方法的影響,會一直運行下去 -
前臺進程可以阻止進程的退出
-
后臺進程不阻止進程的退出
-
創建線程時默認是前臺線程
前臺線程 vs 后臺線程
- 區別標準:是否會阻止進程退出
- ? 前臺線程:進程會等待所有前臺線程執行完畢后才會退出
- ? 后臺線程:進程不等待后臺線程,直接退出時會自動終止所有后臺線程
- 設置方式:通過線程屬性控制
setDaemon()
方法
+++
CPU上的指令是搶占式執行的,都是隨機調度的;main方法作為主線程會和其他子線程進行搶占
Theard.currentTheard
是獲取當前線程的對象
Theard.currentTheard.getName
是獲取當前線程對象的名稱
在線程創建時,如果要表示當前線程的對象,要使用 Theard.currentTheard
+++
線程的狀態
New
:表示創建好了一個Java線程對象,安排好了任務,但是還沒有啟動;在沒有調用start()
方法之前是不會創建PCB的,和PCB沒有任何關系Runnable
:運行+就緒的狀態,在執行任務時最常見的一種狀態之一,在系統有對應的PCBBlocked
:加入synchronized
關鍵字之后,其他線程在等待鎖資源的時候出現的狀態,阻塞中的一種Wait()
:沒有等待時間,一直死等,直到被喚醒 ------wait()
、join()
Time_Waiting
:指定了等待時間的阻塞狀態,過時不候-----wait(time)
、sleep(time)
、join(time)
Terminated
:結束,完成狀態,PCB已經銷毀,但是java線程對象還在
線程等待時需不需要設置等待時間?
不一定,要根據具體的業務需求來確定
補充函數式接口的知識
函數式接口(Functional Interface)概念:
? 函數式接口是 Java 8 引入的核心概念,指有且僅有一個抽象方法的接口(可以包含默認方法或靜態方法)。它的核心作用是為 Lambda 表達式 和 方法引用 提供類型支持,簡化函數式編程。
核心規則
- 必須包含且僅包含 一個抽象方法(
default
和static
方法不計數)。 - 推薦使用
@FunctionalInterface
注解顯式標記,編譯器會強制檢查是否符合規則。 - 常見的函數式接口:
Runnable
、Comparator
、Supplier
、Consumer
、Function
等。
創建接口實例的幾種方式
1. 傳統方式:實現類
// 定義接口
interface MyInterface {void doSomething();
}// 實現類
class MyImpl implements MyInterface {@Overridepublic void doSomething() {System.out.println("傳統實現類");}
}// 實例化
MyInterface obj = new MyImpl();
obj.doSomething(); // 輸出:傳統實現類
2. 匿名內部類
MyInterface obj = new MyInterface() {@Overridepublic void doSomething() {System.out.println("匿名內部類");}
};
obj.doSomething(); // 輸出:匿名內部類
3. Lambda 表達式(最常用)
@FunctionalInterface
interface MathOperation {int calculate(int a, int b);
}// Lambda 實例化
MathOperation add = (a, b) -> a + b;
System.out.println(add.calculate(2, 3)); // 輸出:5
線程安全
在多線程的環境中,程序運行結果不及預期,稱為線程不安全現象
造成線程不安全的原因:
- 線程是搶占式執行的(指令的執行順序隨機的)
- 多個線程修改了同一個變量(共享變量)
多個線程修改同一個變量,會出現線程安全問題
多個線程修改不同的變量,不會出現線程安全問題
一個線程修改變量,不會出現線程安全問題(無論這個變量是否為同一個變量)
- 指令執行過程中不能保證原子性
指令要么全部執行,要么指令全都不執行
由于CPU執行指令不是原子性的,導致某個線程中的指令沒有被全部執行(沒有進行store存儲)就被CPU調度走了,另外的線程加載到的值就是原始值;當兩個線程分別完成自增操作之后把值寫回內存時發生了覆蓋現象
- **工作內存與主內存不同步:**線程修改共享變量后未及時刷回主內存,其他線程讀取舊值
- **JMM內存隔離:**默認情況下線程操作是基于工作內存的
JMM (Java Memory Model) Java內存模型
1. 每一個線程都有自己的工作內存,這些工作內存相互之間不可見
2. Java線程首先從主內存中讀取變量的值到自己的工作內存
3.線程在自己的工作內存中把值修改好之后再把修改之后的值刷回到主內存
4.工作內存與線程之間是一一對應的
以上執行的count++操作,由于實在兩個線程上執行,每個線程都有自己的工作內存,且相互之間不可見,最終導致了線程安全問題
- 線程對共享變量的修改,線程之間相互感知不到
- 工作內存是Java層面對物理層面的關于程序所使用到了寄存器的抽象
- 如果通過某種方式 讓線程之間可以相互通信,稱之為內存可見性
關于JMM的面試題:JMM的規定
- 保證原子性
- 內存可見性
- 指令重排序(有序性)
- 所有的線程不能直接修改內存中的共享變量
- 如果要修改共享變量,需要把這個變量從主內存中復制到自己的工作內存中,修改完成之后再刷回到主內
- 各個線程之間不能相互通信,做到了內存級別的線程隔離
+++
- 程序在編譯執行時可能會出現指令重排序
我們寫的代碼在編譯之后可能會與代碼的指令順序不同,這個過程就是指令重排序
JVM層面可能發生(編譯時)重排,CPU執行指令時也可能發生重排
發生重排的是某個方法或者整個程序的指令,而不是某一條語句
指令重排序必須要保證程序的運行結果一定是正確的
+++
解決線程安全問題
關鍵字 synchronized
使用
synchronized
關鍵字修飾方法或者代碼塊進行加鎖;假設線程t1 先拿到了鎖,如果其他線程想要執行被鎖住的代碼,就必須要等待線程t1先執行完被鎖住的代碼并釋放鎖,如果線程t1沒有釋放鎖,那么其他線程只能阻塞等待,這個線程狀態就是**BLOCK
**
線程安全代碼示例:
對同一個變量進行累加10000次,創建線程 t1 和 t2 分別對同一個變量進行累加操作
public class Demo_302 {public static void main(String[] args) throws InterruptedException {//實例化counter對象thread_safe.Counter302 counter = new thread_safe.Counter302();//創建線程t1Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.counter();}});//創建線程t2Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.counter();}});//啟動線程t1,t2t1.start();t2.start();//等待線程執行完畢t1.join();t2.join();//打印最終結果System.out.println("count = " + counter.count);}}class Counter302 {public int count = 0;/*** 對count進行累加操作*/public void counter() {count++;}}
關鍵字 synchronized
既可以修飾方法,也可以修飾代碼塊
synchronized
修飾方法
class Counter302 {public int count = 0;/*** 對count進行累加操作*/public synchronized void counter() {//真實業務中,在執行加鎖的代碼塊之前有很多的數據獲取或者其他的可 //以并行執行的邏輯//1、從數據庫中查詢數據 selectAll();//2、對數據進行處理 build();//3、對其他的非修改共享變量的方法//.............//當執行到修改共享變量count++;}
}
通過使用
synchronized
關鍵字進行修飾方法t1先獲取了鎖,然后執行方法,方法執行完成后,釋放鎖。然后其他線程再獲取鎖,這樣就由多線程變成了一個單線程運行的狀態,其實就是把多線程轉成了單線程,從而解決了線程安全問題
synchronized
修飾代碼塊
使用
sychronized
修飾代碼塊會解決 方法單線程執行的問題 ,從而提升效率
所以建議使用synchronized
修飾那些修改共享變量的代碼,這樣就可以對那些只操作共享變量的代碼進行上鎖,而那些非操作共享變量的代碼或方法就能以多線程的方式執行
+++
synchronized
特性
- 實現了原子性
- 保證了內存可見性
- 不保證有序性(不會禁止指令重排序)
**關于原子性和CPU調度的關系:**某一個方法或代碼塊實現原子性并不影響CPU調度,在加鎖的代碼塊中,當執行這些指令的時候,比如
LOCK LOAD ADD STORE UNLOCK
這些指令的時候,雖然在執行前已經拿到了鎖,但是在執行過程中 也可能出現該線程被調出CPU的情況,這時其他線程想要執行該指令就必須拿到鎖,但是該鎖已經被上一個線程所持有,因此現在的線程不能執行該指令,只能等待,狀態是BLOCK
保證了內存可見性后一個線程讀到的數據永遠是前一個線程執行完所有指令后刷回到主內存的值,這時主內存就相當于一個交換空間,線程一次寫入和讀取,而且是串行(順序執行)的過程,通過這樣的方式實現了內存可見性。但這并不是實際上的內存可見性,沒有進行任何技術上的操作
+++
關于synchronized
- 被
synchronized
修飾的代碼會變成串行執行 synchronized
可以修飾方法,也可以修飾代碼塊- 被
synchronized
修飾的代碼并不是一次性在CPU上執行完,而是中途可能被CPU調度走,當所有指令執行完成之后才會釋放鎖 - 只給一個線程加鎖,也會出現線程安全問題
關鍵字 volatile
- 只要在多線程環境中修改了共享變量,只管給共享變量加
valotile
- 用
volatile
修飾的變量,由于前后有內存屏障,保證了指令的執行順序;也可以理解為 告訴編譯器,不要進行指令重排序 valotile
不保證原子性
volatile
- 解決了內存可見性
- 解決了有序性
- 不保證原子性
面試題:JMM如何實現原子性,內存可見性,有序性?
-
synchronized
實現了 原子性,由于是串行執行的從而也實現了可見性 -
volatile
真正的實現了內存可見性,有序性
wait() 和 notify() 、wait() 和 sleep()
wait()
和notify()
必須搭配synchronized
使用(即配合鎖一起使用)wait()
和notify()
必須在synchronized
代碼塊中或在其包裹的方法中使用wait()
和join()
的比較:join()
是等待一個線程執行完畢,而wait()
是等待資源準備完成wait()
是Object
類中的方法,join()
是Thread
類中的方法
1.wait()
vs join()
共同點:
- 都會讓當前線程進入 等待狀態。
- 都可以被
InterruptedException
中斷。
核心區別:
特性 | wait() (來自 Object 類) | join() (來自 Thread 類) |
---|---|---|
作用 | 讓當前線程釋放鎖,并等待其他線程喚醒 | 等待目標線程執行完畢 |
鎖的釋放 | 釋放鎖(必須在同步代碼塊中調用) | 不直接釋放鎖,但內部通過 wait() 釋放鎖(依賴目標線程的鎖) |
調用方式 | obj.wait() | thread.join() |
喚醒條件 | 需要其他線程調用 notify() /notifyAll() | 目標線程執行完畢 |
使用場景 | 線程間的協調(如生產者-消費者模型) | 等待子線程結束后再繼續執行主線程 |
+++
2. wait()
vs sleep()
共同點:
- 都會讓當前線程進入 等待/阻塞狀態。
核心區別:
特性 | wait() (來自 Object 類) | sleep() (來自 Thread 類) |
---|---|---|
鎖的釋放 | 釋放鎖(必須搭配synchronized 使用,并在同步代碼塊中調用) | 不釋放鎖(即使當前線程持有鎖) |
所屬類 | Object 類的方法 | Thread 類的靜態方法 |
喚醒條件 | 需要其他線程調用 notify() /notifyAll() 或者 過了等待時間后 | 時間到期后自動恢復 |
使用場景 | 線程間協調(依賴鎖的釋放與獲取) | 單純讓線程暫停一段時間(不涉及鎖協調) |
調用方式 | obj.wait() 一般是鎖對象.wait() | Thread.sleep(ms) |
線程拋出 InterruptedException
的常見場景
-
join()
等待線程終止Thread thread = new Thread(() -> {// 耗時操作 }); thread.start(); try {thread.join(); // 當前線程阻塞,等待 thread 終止 } catch (InterruptedException e) {// 當前線程在等待過程中被其他線程中斷System.out.println("等待 thread 時被中斷!"); }
sleep()
線程休眠
try {Thread.sleep(1000); // 線程休眠 1 秒 } catch (InterruptedException e) {// 休眠期間被中斷System.out.println("休眠被中斷!"); }
wait
等待對象鎖
synchronized (lock) {try {lock.wait(); // 線程釋放鎖并進入等待狀態} catch (InterruptedException e) {// 等待期間被中斷System.out.println("等待鎖時被中斷!");} }
-
阻塞隊列操作(如
BlockingQueue
)
BlockingQueue<String> queue = new LinkedBlockingQueue<>();
try {queue.take(); // 從隊列中取元素(隊列為空時阻塞)
} catch (InterruptedException e) {// 阻塞期間被中斷System.out.println("隊列操作被中斷!");
}
Future.get()
等待異步結果
try {//獲取任務的返回值Integer ret = futureTask.get();//打印返回值System.out.println("運算結果為:" + ret);} catch (InterruptedException e) {throw new RuntimeException(e);} catch (ExecutionException e) {throw new RuntimeException(e);}
總結
- 拋出
InterruptedException
的場景:
所有會阻塞線程的方法(如join()
、sleep()
、wait()
、BlockingQueue
操作等) - 正確處理中斷:
捕獲異常后,通常需要重新設置中斷標志或終止線程
類加載的時候是指 .class
文件從磁盤加載到JVM中的時候,同時生成一個類對象
單例模式(單例類的創建)
餓漢模式
餓漢模式是指在類加載的時候就初始化成員變量
首先定義一個單例類的成員變量
用
static
修飾,保證全局唯一使用
private
關鍵字修飾,確保這個成員變量只能在當前類中使用,禁止外部類調用或更改此成員變量由于使用了
private
關鍵字修飾,所以另外要為變量的獲取定義一個專門的方法getInstance()
既然是單例模式,就不想出現
new
這個對象,因此利用語法 使構造方法私有化,也就是用private
修飾構造方法
new
關鍵字的作用
new
關鍵字在創建對象時主要完成以下操作:
- 分配內存:為對象在堆內存中開辟空間。
- 初始化默認值:將對象的成員變量賦予默認值(如
int
默認是0
,引用類型默認是null
)。 - 調用構造方法:執行類的構造方法(用于進一步初始化成員變量或執行其他邏輯)。
- 返回對象引用:將內存地址賦值給引用變量
在外部類中無法調用private
修飾的構造方法,因此就new
不出來對象,利用這樣的語法就避免了出現new
出對象的現象。結果就是使用** 類名+ .** 的方式創建單例類,使用** 類名+ .**的方式獲取單例類對象
具體代碼實現:
public class SingletonHungry {//定義成員變量,用static修飾,保證全局唯一//使用private修飾,確保外部類不能調用或更改此變量private static SingletonHungry instance = new SingletonHungry();//構造方法私有化,避免外部類中出現 new 一個對象的現象private SingletonHungry() {}//提供一個公開的方法 返回instance對象public static SingletonHungry getInstance() {return instance;}
}
懶漢模式
懶漢模式是指等需要的時候再實例化單例類對象,懶加載:不會隨類的加載而創建,而是先賦值為null
+++
不加鎖
不加鎖的懶漢模式,此時在單線程的場景下運行不會出錯,但在多線程的環境下會出錯
public class SingletonLazy {//定義一個全局唯一變量,先賦值為null,需要時再實例化單例類private static SingletonLazy instance = null;//構造方法私有化private SingletonLazy() {}//對外提供一個獲取對象的方法public static SingletonLazy getInstance() {//判斷這個對象是否已經被創建過if (instance == null) {//創建對象instance = new SingletonLazy();}//返回對象return instance;}
}
+++
加鎖
加鎖的懶漢模式,應該在整個if
判斷語句外加鎖
public class SingletonLazy {//定義一個全局唯一變量,先賦值為null,需要時再實例化單例類private static SingletonLazy instance = null;//構造方法私有化private SingletonLazy() {}//對外提供一個獲取對象的方法public static SingletonLazy getInstance() {//對整個判斷語句加鎖synchronized (SingletonLazy.class) {//判斷這個對象是否已經被創建過if (instance == null) {//創建對象instance = new SingletonLazy();}}//返回對象return instance;}
}
+++
雙重檢查鎖DCL
此時解決了多線程的線程安全問題,但在上面的代碼設計上還是有一些問題
當第一個線程執行完所有指令后,此時
instance
不再為null
,剩余的線程以后也永遠不會再執行new
對象的操作了,因此剩余的線程也就沒有必要再加鎖了,剩余的線程只需要返回instance
對象就可以了。java層面的
synchronized
關鍵字對應了CPU上的指令,即LOCK
和UNLOCK
指令,而這兩個指令是互斥鎖,比較消耗系統資源;也就是從第二個線程開始這個加鎖操作都是無效的操作,消耗了大量的系統資源
為了解決這個問題,我們在加鎖之前判斷 instance
是否為null
,為null
就執行加鎖操作,否則直接返回instance
對象,這樣的設計稱之為雙重檢查鎖 DCL
public class SingletonLazy {//定義一個全局唯一變量,先賦值為null,需要時再實例化單例類private static SingletonLazy instance = null;//構造方法私有化private SingletonLazy() {}//對外提供一個獲取對象的方法public static SingletonLazy getInstance() {//判斷instance是否為nullif (instance == null) {//對整個判斷語句加鎖synchronized (SingletonLazy.class) {//判斷這個對象是否已經被創建過if (instance == null) {//創建對象instance = new SingletonLazy();}}}//返回對象return instance;}
}
+++
解決指令重排序問題
new
關鍵字在創建對象時主要完成以下操作:
-
分配內存:為對象在堆內存中開辟空間
-
初始化默認值:將對象的成員變量賦予默認值(如
int
默認是0
,引用類型默認是null
) -
返回對象引用:將對象在內存中的首地址賦值給引用變量
1 和 3 是強相關的關系,2并不強相關
1 必須在 3 之前執行,先分配內存才能把對象的內存地址返回給對象的引用
正常:1 2 3
重排序:1 3 2
如果出現以上的重排序,那么
instance
就是一個沒有初始化完成的對象,使用這個對象的時候,就容易出現問題,比如調用這個對象中的屬性
只要多線程中修改共享變量的問題,就加volatile
修飾,進而解決指令重排序問題
對于內存可見性問題,synchronized
關鍵字就已經通過原子性解決了
DCL最終版代碼:
public class SingletonDCL {//定義一個全局唯一變量,先賦值為null,需要時再實例化單例類private static volatile SingletonDCL instance = null;//構造方法私有化private SingletonDCL() {}//對外提供一個獲取對象的方法public static SingletonDCL getInstance() {//第一次 判斷是否需要加鎖if (instance == null) {//對整個判斷語句加鎖synchronized (SingletonDCL.class) {//判斷這個對象是否已經被創建過if (instance == null) {//創建對象instance = new SingletonDCL();}}}//返回對象return instance;}
}
+++
多線程中, 從主內存加載數據 和 從寄存器加載數據 的兩種情況
在多線程環境中,當兩個線程同時爭奪鎖資源時,沒有獲取到鎖的線程會處于阻塞狀態
BLOCK
,在這個阻塞過程中會發生很多事,當拿到鎖的線程執行完所有的指令并釋放鎖后,另外一個線程拿到鎖后會重新從主內存LOAD
加載所需的數據,類似實現了可見性使用
Thread.sleep()
或者.wait()
對線程進行休眠,也會使線程進入阻塞狀態,當再次進入CPU時會重新加載所需的數據當一個線程從 就緒/運行隊列 轉到 阻塞隊列后,當該線程再次回到 就緒/運行隊列 時會重新從主內存加載數據
如果一個線程執行的任務在一直循環,那么這個線程狀態一直在 就緒 、運行兩個狀態中來回轉換,就緒和運行是屬于同一種PCB狀態;所以不會從主內存中加載數據,只是從寄存器中加載數據
+++
阻塞隊列
性質
- 插入數據時,如果隊列已經滿了,那么就阻塞等待,等到隊列中有空余容量時再插入
- 取出數據時,如果隊列為空,那么就阻塞等待,直到隊列中有數據時再取出
+++
消息隊列的特性
利用阻塞隊列的性質實現的消息隊列
- 解耦
? 生產消息的應用程序把消息寫入消息隊列 -->(生產者)
? 使用消息的應用程序從消息隊列中取出消息
- 削峰填谷(流量)
? 針對流量暴增的時候使用消息隊列來進行緩沖
- 異步操作
同步: 發出請求后,死等,等到有響應的時候再進行下一步操作
異步:發出請求后,就去干別的事了,當響應之后主動通知請求方
+++
阻塞隊列的一些常用方法
拋出異常 | 特殊值 | 阻塞 | 超時 | |
---|---|---|---|---|
插入 | add(e) | offer(e) | put(e) | offer(e, time, unit) |
移除 | remove() | poll() | take() | poll(time,unit) |
檢查 | element() | peek() | 不可用 | 不可用 |
-
add
:add
方法在添加元素的時候,若超出了度列的長度會直接拋出異常 -
offer
方法在添加元素時,如果發現隊列已滿無法添加,會直接返回false
,不會拋異常 -
put
:對于put
方法,若向隊尾添加元素的時候發現隊列已經滿了會發生阻塞一直等待空間,以加入元素 -
offer(E e, long timeout, TimeUnit unit)
:如果在隊列滿的時候,可以等待設定的時間,超時則返回falseremove
:刪除隊首元素,若隊列為空,拋出NoSuchElementException
異常 -
poll
:獲取并刪除隊首元素,如果隊列為空執行poll
,返回null
-
take
:獲取并刪除隊首元素,如果隊列為空,阻塞等待直到隊列中有數據 -
poll(long timeout, TimeUnit unit)
:如果隊列為空,可以等待設定的時間,返回隊首元素或null
+++
自定義實現阻塞隊列
遵循先進先出的規則
- 插入數據時,如果隊列已經滿了,那么就阻塞等待,等到隊列中有空余容量時再插入
- 取出數據時,如果隊列為空,那么就阻塞等待,直到隊列中有數據時再取出
需要注意的一些問題
- 在多線程環境中修改共享變量,對共享變量加
volatile
修飾 - 使用
while()
循環判斷隊列是否為空或者已滿,防止出現**虛假喚醒**。原因:當大量線程積壓在wait()
時,一旦鎖對象notifyAll()
喚醒所有阻塞等待的線程后,此時積壓在wait()
的所有線程就會執行隊列中添加元素的操作,這樣就會造成head
以及以后的下標的值被覆蓋。添加while()
循環判斷條件后,各個線程在喚醒之后會先判斷while
循環中的條件是否成立(隊列已滿還是隊列為空),如果已滿或者為空,則線程繼續等待,否則繼續執行往下的代碼.
+++
代碼示例
package blocking_queue;public class MyBlockingQueue {//定義一個數組來實現隊列,具體容量由構造方法確定private Integer elementDate[];//定義隊首下標private volatile int head = 0;//定義隊尾下標private volatile int tail = 0;//定義隊列中的有效元素個數private volatile int size = 0;//構造方法來初始化隊列容量public MyBlockingQueue(int capacity) {//保證隊列容量必須大于0if (capacity <= 0) {throw new RuntimeException("capacity必須大于0");}elementDate = new Integer[capacity];}//定義一個插入數據的方法public void put(int val) throws InterruptedException {synchronized (this) {//判斷隊列是否已經滿了while (size >= elementDate.length) {//阻塞等待this.wait();}//tail下標位置插入數據elementDate[tail] = val;//tail下標向后移動tail++;//調整tail下標位置while (tail >= elementDate.length) {tail = 0;}//更新sizesize++;//喚醒阻塞等待的線程this.notifyAll();}}//定義一個取出數據的方法public synchronized int take() throws InterruptedException {//判斷隊列是否為空while (size == 0) {//阻塞等待this.wait();}//取出隊首元素int ret = elementDate[head];//head下標向后移動head++;//調整head下標的值while (head >= elementDate.length) {head = 0;}//更新size的值size--;//喚醒阻塞等待的線程this.notifyAll();//返回取出的隊首元素的值return ret;}
}
+++
定時器
自定義實現定時器
核心步驟:
- 創建一個描述任務和執行任務時間的類
MyTask
- 定義添加任務的方法
schedule()
- 定義一個優先級阻塞隊列用來組織任務
- 創建掃描線程,不斷掃描隊列中任務
Lambda表達式中的this
引用的是他所在對象的實例
復習 匿名內部類 和 Lamdba
表達式,鎖對象,Lamdba
表達式中的this引用的是哪個實例?
定時器代碼示例
package timer;import java.lang.management.MemoryUsage;
import java.util.Comparator;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.TimeUnit;//自定義實現定時器
public class MyTimer {//使用阻塞隊列組織任務private BlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();Object locker = new Object();public MyTimer() {//創建一個線程不斷掃描線程Thread thread = new Thread(() -> {while (true) {try {//從隊列中取出任務MyTask task = queue.take();//判斷是否到了任務的執行時間long curTime = System.currentTimeMillis();if (curTime < task.getTime()) {//時間未到,將任務重新放回到隊列中queue.put(task);//計算等待時間long waitTime = task.getTime() - curTime;synchronized (locker) {locker.wait(waitTime);}} else {//時間到了,執行任務task.getRunnable().run();}} catch (InterruptedException e) {throw new RuntimeException(e);}}});//啟動線程thread.start();//創建 一個后臺線程Thread daemonThread = new Thread(() -> {while (true) {synchronized (locker) {//喚醒線程locker.notifyAll();}//休眠一會try {TimeUnit.MILLISECONDS.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}}});//設置為后臺線程daemonThread.setDaemon(true);//啟動后臺線程daemonThread.start();}//創建schedule方法,把任務放入阻塞隊列中public void schedule(Runnable runnable, long delay) throws InterruptedException {MyTask task = new MyTask(runnable, delay);queue.put(task);synchronized (locker) {locker.notifyAll();}}}//創建描述任務和任務執行時間的類
//在阻塞隊列中每個元素的數據類型是MyTask,在隊列中加入任務對象時都會進行排序,這里希望是按照小根堆排序
//所以MyTask這個類要實現Comparable接口來明確具體的比較方法
class MyTask implements Comparable<MyTask> {public MyTask(Runnable runnable, long delay) {//校驗任務不能為空if (runnable == null) {throw new RuntimeException("任務不能為空");}//校驗延時時間不能為負數if (delay < 0) {throw new RuntimeException("延時時間不能為負");}this.runnable = runnable;//計算出任務執行的時間this.time = delay + System.currentTimeMillis();}//任務private Runnable runnable;//任務的執行時間private long time;public long getTime() {return this.time;}public Runnable getRunnable() {return runnable;}@Overridepublic int compareTo(MyTask o) {//return (int) (this.getTime() - o.getTime());//任務執行時間為long類型,避免long類型溢出(Long.MAX_VALUE - (-1) 會溢出)if (this.getTime() < o.getTime()) {return -1;} else if (this.getTime() > o.getTime()) {return 1;} else {return 0;}}
}
線程池
使用線程池的優勢及作用
線程池是什么:字面意思就是,一次創建很多個線程,用的時候從池子里面拿出一個,用完之后換回
優勢:避免了頻繁的創建銷毀線程的開銷,提高了程序的性能
作用:用少量的線程執行大量的任務
++++
JDK提供的幾種線程池
public static void main(String[] args) {// 1. 用來處理大量短時間工作任務的線程池,如果池中沒有可用的線程將創建新的線程,如果線程空閑60秒將收回并移出緩存ExecutorService cachedThreadPool = Executors.newCachedThreadPool();// 2. 創建一個操作無界隊列且固定大小線程池ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);// 3. 創建一個操作無界隊列且只有一個工作線程的線程池ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();// 4. 創建一個單線程執行器,可以在給定時間后執行或定期執行。ScheduledExecutorService singleThreadScheduledExecutor = Executors.newSingleThreadScheduledExecutor();// 5. 創建一個指定大小的線程池,可以在給定時間后執行或定期執行。ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);// 6. 創建一個指定大小(不傳入參數,為當前機器CPU核心數)的線程池,并行地處理任務,不保證處理順序Executors.newWorkStealingPool();}
}
這里使用類名加**.**的方式來獲取對象的模式稱之為 工廠方法模式,即根據不同的業務需求定義不同的方法來獲取對象
+++
創建線程池的七個參數
++++
用現實場景解釋線程池原理
銀行排號系統
場景:
- 銀行中平時只開兩個核心窗口為客服提供服務,相當于線程池中的核心線程數
- 當有新客戶來時,如果這兩個核心窗口空閑,可以直接辦理業務
- 如果兩個核心窗口都在處理業務時,客戶就去等待區(最多容納20人)等待,這個等待區就相當于阻塞隊列,阻塞隊列的容量是20
- 隨著等待的人數越來越多,等待區已經滿了,那么銀行就會開放其他的窗口一起辦理業務,就是創建臨時線程
- 再進入銀行的客戶(阻塞隊列已滿),就會執行拒絕策略
++++
分析創建線程池的源碼
new ThreadPoolExecutor
,new
一個線程池對象
-
調用
submit
方法向線程池中提交任務 -
點進execute的源碼
+++
線程池流程圖
- 添加任務,判斷當前線程數是否達到核心線程數,沒有就創建線程執行這個任務;
- 如果達到了核心線程數,再判斷隊列是否已滿,如果隊列沒滿,就將任務加入隊列等待;
- 如果隊列滿了,再判斷當前線程總數是否達到最大線程數,如果沒有達到,就創建臨時線程執行任務;
- 如果達到了最大線程數,就執行拒絕策略
++++
自定義實現線程池
- 用
Runnable
描述任務 - 使用阻塞隊列組織管理任務
這里使用阻塞隊列的好處:當隊列中沒有任務的時候就等待,節省了系統資源
-
提供一個向隊列中添加任務的方法
-
創建多個線程,掃描隊列中的任務,有任務的時候就取出來執行即可
+++
代碼示例:
package thread_pool;import java.util.concurrent.*;/*** @Description:* @Auther: pdq* @Date: 2025/4/8 19:39*/
public class MyThreadPool {private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(10);//創建掃描線程public MyThreadPool(int threadNum) {if(threadNum < 1) {throw new IllegalArgumentException("線程數量不能小于1");}for (int i = 0; i < threadNum; i++) {//創建線程,并不斷掃描Thread thread = new Thread(() -> {while (true) {try {//從隊列中取出任務Runnable task = queue.take();task.run();} catch (InterruptedException e) {throw new RuntimeException(e);}}});//啟動線程thread.start();}}/*** @Description:* 執行提交任務的方法* @param runnable* @return: void*/public void submit(Runnable runnable) throws InterruptedException {//避免任務是否為nullif(runnable == null) {throw new IllegalArgumentException("任務不能為空");}//將任務提交到隊列queue.put(runnable);}}
++++
線程池中的拒絕策略直接拒絕
- 直接拒絕
new ThreadPoolExecutor.AbortPolicy()
- 返回給調用者
new ThreadPoolExecutor.CallerRunsPolicy()
- 放棄目前最早等待的任務
new ThreadPoolExecutor.DiscardOldestPolicy()
- 放棄最新提交的任務
new ThreadPoolExecutor.DiscardPolicy()
其中只有 直接拒絕 的策略是會拋出異常的,其他拒絕策略不會拋出異常
++++
鎖策略
樂觀鎖 VS 悲觀鎖
-
**樂觀鎖:**在執行任務之前預期競爭不激烈,那就可以先不加鎖,等后面如果真實發生了鎖競爭再加鎖
-
**悲觀鎖:**在執行任務之前預期競爭非常激烈,必須先加鎖再執行任務
在競爭非常激烈時,會發生鎖沖突
樂觀鎖和悲觀鎖主要是從加鎖的態度上去考慮問題
- 樂觀鎖一旦發生了鎖沖突就會加鎖
+++
輕量級鎖 VS 重量級鎖
-
**輕量級鎖:**加鎖的過程比較簡單,用到的資源比較少,典型的就是用戶態的一些操作(JVM層面就可以完成加鎖)
-
**重量級鎖:**加鎖的過程比較復雜,用到的資源比較多,典型的就是內核態的一些操作
輕量級鎖和重量級鎖主要是從加鎖的過程上去考慮問題
- 樂觀鎖是能不加鎖就不加鎖,從而導致他干活少,消耗的資源也少,所以可以說樂觀鎖就是一種輕量級鎖
- 悲觀鎖是任何時候都加鎖,從而導致他干活多,消耗的資源也多,所以可以說悲觀鎖就是一種重量級鎖
- 輕量級鎖:一會問一下鎖釋放了沒,在不停地自旋,可以第一時間知道鎖是否被釋放
- 重量級鎖:不會第一時間知道鎖是否被釋放,一直等到其他線程來喚醒
++++
自旋鎖 VS 掛起等待鎖
-
**自旋鎖:**不停地檢查鎖是否被釋放,如果一旦鎖被釋放就可以直接獲取到鎖資源
-
**掛起等待鎖:**阻塞等待,等待到被喚醒
這里的自旋鎖和掛起等待鎖是鎖的真正的實現,可以獲取到真正的對象,而樂觀鎖和悲觀鎖、輕量級鎖和重量級鎖 只是實現的模板
這兩種鎖的優缺點:
- 自旋鎖: 純用戶態的操作,可以第一時間獲取到鎖;有自旋次數和時間的限制,通過這個限制可以控制對系統的消耗
- 掛起等待鎖:內核態的操作,會生成對應的加鎖指令,要等待喚醒,在等待的過程中會釋放CPU資源
+++
讀寫鎖 VS 普通互斥鎖
- 讀寫鎖:
? 分為 讀鎖 和 寫鎖
- 讀鎖:讀操作的時候加讀鎖(共享鎖),多個讀鎖可以共存,同時加多個讀鎖互不影響
- 寫鎖:寫操作的時候加寫鎖(排他鎖),只允許有一個寫鎖執行任務,寫鎖和其他鎖是沖突的
寫鎖寫鎖不能共存
寫鎖和讀鎖也不能共存
讀鎖和讀鎖可以共存
為什么要使用讀寫鎖?
在程序運行的過程中,并不是所有的操作都需要修改數據,但又希望在讀數據的時候其他線程不要來修改數據,讀與寫不能同時加鎖
當執行寫操作的時候,不希望其他任何線程來讀數據,當寫完之后才可以繼續對數據進行讀取,就可以加寫鎖(排他鎖)
- 普通互斥鎖:
有競爭關系,只能一個線程釋放了鎖資源之后,其他線程才可以來搶,之前用到的鎖基本上都是互斥鎖,寫鎖也是一個互斥鎖
+++
公平鎖 VS 非公平鎖
-
公平鎖: 先來后到,先排隊的線程先拿到鎖,后排隊的線程后拿到鎖
-
非公平鎖: 大家去爭搶,誰搶到就是誰的
一般情況下,大多數的鎖都是非公平鎖
這樣的情況就類似于:
現實生活中如果想要真正的公平:在 立法、執法、教育、環境等各個方面都要發揮作用;因此這樣會消耗更大的資源,實現公平鎖的過程也是一樣的,需要用額外的邏輯去管理線程,做到先來后到
Java中的JUC有一個類專門實現了公平鎖
+++
可重入鎖 VS 不可重入鎖
-
可重入鎖:對一把鎖可以連續加多次,不造成死鎖(多次加鎖也要多次解鎖)
-
不可重入鎖:對一把鎖可以連續加多次,造成死鎖
+++
synchronized是什么鎖?
- 既是樂觀鎖也是悲觀鎖
- 既是輕量級鎖也是重量級鎖
- 既是自旋鎖也是掛起等待鎖
- 是互斥鎖
- 是非公平鎖
- 是可重入鎖
synchronized在競爭不激烈的時候,是自旋鎖、輕量級鎖、樂觀鎖
在競爭激烈的時候,是掛起等待鎖、重量級鎖、悲觀鎖
程序員不需要關注競爭是否激烈,因為synchronized內部已經幫我們實現好了,我們只需要關注自己的業務即可
++++
CAS(Compare And Swap)
什么是CAS
CAS:全稱 compare and swap
,字面意思:“比較并交換”,一個CAS涉及以下操作:
CAS偽代碼:
boolean CAS(address, expectValue, swapValue) {if (&address == expectedValue) {&address = swapValue;return true;}return false;
}
CAS參數列表中的參數含義:
address
:表示要修改值的內存地址,即需要修改的共享變量的地址expectValue
:表示預期值,執行CAS之前讀取的預期值,即線程認為變量應該是什么值swapValue
:表示要設置的新值,希望將變量更新為的值
CAS執行流程
- 先加載LOAD出預期值,用這個預期值和內存中的做比較
- 如果預期值和內存中的值相等,就用新的值更新內存中的值
- 如果預期值和內存中的值不相等,就進入下一次CAS
在執行CAS指令時
- 先加載預期值LOAD,這個LOAD對應是JAVA層面
- 再執行具體的操作(ADD)
- 最后執行CAS指令(在CAS指令中也會有一條LOAD操作,這個LOAD操作是讀取主內存中的值,將該值與預期值進行比較,相等就會把要更新的值寫入主內存)
整個過程是原子性的(通過CPU指令cmpxchg
實現)
+++
CAS的應用
實現自旋鎖
偽代碼:
public class SpinLock {private Thread owner = null;public void lock(){// 通過 CAS 看當前鎖是否被某個線程持有.// 如果這個鎖已經被別的線程持有, 那么就?旋等待.// 如果這個鎖沒有被別的線程持有, 那么就把 owner 設為當前嘗試加鎖的線程.while(!CAS(this.owner, null, Thread.currentThread())){}}//釋放鎖public void unlock (){this.owner = null;}}
-
自旋是通過while把CAS進行包裹,讓CAS沒有成功的時候不停的執行,直到成功執行為止
-
自旋是在用戶態實現的鎖,是輕量級鎖
-
**我們看到的CAS執行的這么多的操作(去讀取,去比較,去修改),其實對應的是一條指令, cmpxchg**指令
-
cmpxchg是指令級別的操作,從CPU層面做了原子性支持,CPU層面即是硬件層面的支持
+++
實現原子類
? 標準庫中提供了java.util.concurrent.atomic
包,里面的類都是基于這種方式實現的,典型的就是AtomicInteger
類。其中的getAndIncrement
相當于i++操作
代碼示例:
public static void main(String[] args) {//原子類AtomicInteger atomicInteger = new AtomicInteger();//獲取當前值 0System.out.println(atomicInteger.get());//自增 i++ 1atomicInteger.getAndIncrement();System.out.println(atomicInteger.get());//先自增 ++i 2atomicInteger.incrementAndGet();System.out.println(atomicInteger.get());//自減 i-- 1atomicInteger.getAndDecrement();System.out.println(atomicInteger.get());//先自減 --i 0atomicInteger.decrementAndGet();System.out.println(atomicInteger.get());//自增100 i+100 100atomicInteger.getAndAdd(100);System.out.println(atomicInteger.get());}
CAS的ABA問題
什么是ABA問題
ABA 問題發生在以下場景:
- 線程 1 讀取共享變量
V
的值為A
- 線程 1 被掛起,線程 2 開始執行:
- 將
V
的值從A
修改為B
- 再將
V
的值從B
修改回A
- 將
- 線程 1 恢復執行,發現
V
的值仍為A
,于是 CAS 操作成功,將V
更新為新值B
問題本質:
雖然最終 V
的值從 A
變為 B
,但中間的 A → B → A
修改可能破壞了程序邏輯的正確性
ABA問題的危害
- 車主 A 打開 App
- 看到剩余車位為
1
,點擊“預訂”按鈕。 - 系統讀取當前剩余車位值為
1
(預期值 A)。
- 看到剩余車位為
- 車主 B 搶先預訂
- 車主 B 同時預訂,成功將車位從
1
改為0
,并入場停車。
- 車主 B 同時預訂,成功將車位從
- 車主 B 臨時離開
- 車主 B 在 5 分鐘后駕車離開,系統將剩余車位從
0
恢復為1
(值從 B 變回 A)。
- 車主 B 在 5 分鐘后駕車離開,系統將剩余車位從
- 車主 A 的預訂操作繼續執行
- 系統執行 CAS 操作:檢查剩余車位是否仍為
1
(預期值 A)。 - 由于值已恢復,CAS 成功,車位被改為
0
,車主 A 收到“預訂成功”。
- 系統執行 CAS 操作:檢查剩余車位是否仍為
問題后果
- 車位超售:實際僅 1 個車位,但系統允許多個車主預訂。
- 現場沖突:車主 A 到達后發現車位已被占用,引發糾紛。
+++
解決方案:加入版本號/標記
-
通過為共享變量附加一個 版本號(Version) 或 標記(Stamp),每次修改遞增版本號,確保值的修改歷史唯一性
-
在每次修改時同時更新值和版本號
初始狀態:value = A, version = 0
第一次修改:value = B, version = 1
第二次修改:value = A, version = 2
- CAS操作需同時檢查比較值和版本號
只有預期值和內存中的值相等,并且對應的版本號也相等,才能更新內存中的值
+++
鎖升級
鎖升級的過程:無鎖 ----> 偏向鎖 ----> 輕量級鎖(自旋鎖) ----> 重量級鎖
**鎖升級的目的:**根據線程競爭情況動態調整鎖的級別來平衡性能與線程安全;根據線程競爭情況動態選擇最優鎖策略,減小性能開銷
各階段鎖的對比:
鎖狀態 | 適用場景 | 實現方式 | 性能開銷 | 同步策略 |
---|---|---|---|---|
無鎖 | 無競爭 | 無同步操作 | 無 | - |
偏向鎖 | 單線程重復訪問 | CAS記錄線程ID | 極低 | 無競爭時直接訪問 |
輕量級鎖 | 多線程交替執行(低競爭) | CAS + 自旋 | 低 | 自旋嘗試,避免阻塞 |
重量級鎖 | 高并發競爭 | 操作系統互斥量(Mutex) | 高 | 線程阻塞,依賴CPU調度 |
鎖升級的四個階段:
- 無鎖:
- 場景:未被任何線程訪問
- 觸發升級:首次被線程訪問時,會根據競爭情況升級為偏向鎖或輕量級鎖
- 偏向鎖:
-
場景:單線程重復進入同步塊(單線程獨占訪問同步塊)
-
特點:對象頭的
MarkWord
會記錄偏向線程ID,該偏向線程再次訪問同步塊時無需CAS操作 -
觸發升級:當其他線程嘗試進入同步塊時(嘗試獲取鎖),偏向鎖被撤銷,升級為輕量級鎖
- 輕量級鎖(自旋鎖):
- 場景:多線程順序交替執行同步塊,線程間無激烈競爭,適合短時間鎖占用
- 自旋優化:在升級為重量級鎖之前會進行短暫自旋(循環嘗試CAS),避免立即阻塞
- 觸發升級:發輕量級鎖自旋失敗(競爭激烈)或 檢測到多線程競爭
- 重量級鎖:
- 場景:長時間持有鎖或高并發競爭
+++
-
鎖升級是單向不可逆的,一旦升級為重量級鎖,即使后序競爭消失,也不會降級
-
偏向鎖的延遲啟用:JVM默認在啟動4秒后才會啟用偏向鎖,避免啟動階段大量加載類和初始化時的鎖競爭 導致頻繁撤銷偏向鎖
-
偏向鎖的撤銷條件:
1. <font color = red>當有其他線程嘗試進入同步塊時,撤銷偏向鎖升級為輕量級鎖或重量級鎖</font>1. <font color = red>原偏向線程不再存活,撤銷偏向鎖</font>
+++
synchronized鎖消除
? 編譯器和JVM可以判斷鎖是否可以消除,如果可以,就直接消除
程序員在寫代碼的時候,可以自由決定加
synchronized
的時間,也就是什么時候加,什么時候不加,完全由程序員決定;但是代碼在編譯運行的時候JVM就可以知道加了synchronized
的代碼塊中,對共享變量是執行讀操作還是寫操作,還知道當前線程是多線程狀態還是單線程狀態如果所有加了
synchronized
的代碼塊,并沒有對共享變量執行寫操作,那么synchronized
對應的鎖就不會生效(不會編譯成LOCK
指令)線程安全問題只有多個線程對共享變量進行寫操作時才會發生,如果沒有寫操作,那么加
synchronized
就沒有必要,所以JVM不會真的去加鎖,這個現象叫做鎖消除
+++
synchronized鎖粗化
程序員寫代碼的時候,什么時候加synchronized
,什么時候不加,JVM管不了
業務流程如下:
眾所周知加鎖與釋放鎖的過程是很消耗CPU資源的,所以JVM認為這種加鎖的方式是很低效的,會進行優化,也就是從方法1開始加鎖,到方法4執行完釋放鎖,整個過程只有一個加鎖操作
把方法級別的細粒度鎖,優化成業務級別的粗粒度鎖
+++
JUC
? JUC(java.util.concurrent
)是多線程環境中用的比較多的一個包,復雜度也比較高,包中提供了一些API(應用程序接口),整個多線程的處理都是java層面實現的
Callable接口
Callable 和 Runnable接口一樣都是函數式接口,都可以使用
lambda
表達式簡化創建寫法Callable中的call()方法 和 Runnable接口中的run()方法相同,都是定義線程任務的方法
Callable中的call()方法有返回值V,同時可以向外部拋出業務異常,而Runnable方法中的run()方法即沒有返回值也不可以向外拋出異常,業務異常只能在run()方法內部處理
通過Callable 定義任務
//創建Callable接口實例,并定義任務Callable<Integer> callable = new Callable<Integer>() {@Overridepublic Integer call() throws Exception {System.out.println("正在運算過程中....");int sum = 0;for (int i = 1; i <= 1000; i++) {sum += i;}//休眠3秒,模擬真實業務處理的時間TimeUnit.SECONDS.sleep(3);//throw new Exception("執行過程中出現了異常....");System.out.println("運算成功");return sum;}};
定義完任務,如何將任務添加進Thread
類
//Callable要配合FutureTask一起使用,FutureTask用來獲取Callable的執行結果FutureTask<Integer> futureTask = new FutureTask<>(callable);
//FutureTask當做構造參數傳入到Thread構造方法中Thread thread = new Thread(futureTask);//啟動線程thread.start();
如何獲取call()方法的返回值
//獲取任務的返回值,等待結果的時候可能會被中斷,會拋出InterruptionException異常
try {//調用futureTask.get()方法獲取call()方法的返回值//調用futureTask.get()方法時,當前線程會阻塞等待(后面的代碼不會執行),一直等到call()方法有一個運行結果Integer ret = futureTask.get();//打印返回值System.out.println("運算結果為:" + ret);
} catch (InterruptedException e) {e.printStackTrace();
} catch (ExecutionException e) {e.printStackTrace();//打印異常信息System.out.println("打印日志: " + e.getMessage());
}
+++
Runnable 和 Callable的區別
Callable
接口中的call()
方法有返回值,Runnable
接口中的run()
方法沒有返回值Callable
中的call()
方法可以拋出異常,Runnable
接口中的run()
方法不能拋出異常Callable
要配合FutureTask
一起使用,通過futureTask.get()
方法獲取call()
方法的返回值- 兩者都是描述線程任務的接口
+++
創建線程的幾種方式
- 繼承
Thread
類,實現run()
方法 - 實現
Runnable
接口,并實現run()
方法 - 實現
Callable
接口,并實現call()
方法 - 通過創建線程池,并提交任務
++++
ReentrantLock
( 可重入鎖)
? ReentrantLock
是java中的一個類,使用時要創建一個對象
//初始化一個鎖ReentrantLock lock = new ReentrantLock();try {//加鎖lock.lock();//執行加鎖的代碼} finally {//釋放鎖lock.unlock();}//嘗試加鎖lock.tryLock();//嘗試加鎖,并設置等待時間lock.tryLock(1, TimeUnit.SECONDS);
使用 try...finally
把 lock.unlock
釋放鎖的代碼放入到finally
中,確保釋放鎖的代碼能夠執行
正確的使用
// 初始化一個鎖ReentrantLock lock = new ReentrantLock();try {// 開始執行業務代碼之前先上鎖lock.lock();System.out.println("業務代碼執行中....");TimeUnit.SECONDS.sleep(3);throw new Exception("執行出現異常");} finally {// 無論任何時候都可以釋放鎖lock.unlock();System.out.println("鎖已釋放");}
+++
公平鎖 和 非公平鎖的創建
//創建一個公平鎖ReentrantLock lock = new ReentrantLock(true);//創建一個非公平鎖ReentrantLock lock1 = new ReentrantLock(false);
+++
讀寫鎖的創建 和 使用
//創建一個讀寫鎖ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();//獲取讀鎖ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();//獲取寫鎖ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();//讀鎖加鎖,共享鎖,多個讀鎖可以共存writeLock.lock();//讀鎖解鎖writeLock.unlock();//寫鎖加鎖,排他鎖,多個鎖不能共存readLock.lock();//寫鎖解鎖readLock.unlock();
休眠和喚醒
? 不同于使用synchronized
包裹的休眠和喚醒( 鎖對象.wait() 和 鎖對象.notify() ),這里使用
Condition
可以綁定到多個鎖,可以實現對一部分符合相應條件的線程進行喚醒
public static void main(String[] args) throws InterruptedException {ReentrantLock lock = new ReentrantLock();//定義很多個休眠與喚醒條件//條件1Condition male = lock.newCondition();//條件2Condition female = lock.newCondition();//根據不同的條件進行阻塞等待male.await();//根據不同的條件進行喚醒male.signal(); //喚醒相應隊列中的一個線程male.signalAll(); //喚醒相應隊列中的所有線程//根據不同的條件進行阻塞等待female.await();//根據不同的條件進行喚醒female.signal(); //喚醒相應隊列中的一個線程female.signalAll(); //喚醒相應隊列中的所有線程}
+++
ReentrantLock
和 synchronized
的區別
+++
信號量 (Semaphore)
信號量,用來表示“可用資源的個數”. 本質上就是一個計數器
理解信號量:
停車場:
- 停車場外面有一個顯示牌,牌子上會顯示當前停車場的可用空位個數
- 每進入一輛車,顯示牌上的可用個數就減1; 每出去一輛車,可用個數就加1
- 如果停車場的車位都占滿了,那么顯示牌上就顯示車位已滿,這時在停車場外的車就要阻塞等待
阻塞之后,每出去一輛車,個數減1,意味著釋放了資源,外面等待的車就可以進入
停車場模擬代碼示例:
public static void main(String[] args) {//創建信號量,初始化可用資源 5 個,相當于有5個停車位Semaphore semaphore = new Semaphore(5);//定義線程的任務Runnable runnable = new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "開始申請資源...");try {//申請資源,相當于進入停車場,可用車位減1semaphore.acquire();System.out.println(Thread.currentThread().getName() +"======已經申請到資源=======");//處理業務邏輯,用休眠模擬,相當于停車時間TimeUnit.SECONDS.sleep(1);//釋放資源,相當于出停車場,可用車位加1semaphore.release();System.out.println(Thread.currentThread().getName() + "-------釋放資源...");} catch (InterruptedException e) {throw new RuntimeException(e);}}};//創建15個線程來執行任務,相當于有15輛車要進入停車場for (int i = 0; i < 15; i++) {//創建線程并指定任務Thread thread = new Thread(runnable);//啟動線程thread.start();}}
+++
通過信號量可以限制系統中并發執行的線程個數
++++
CountDownLatch
場景:100米跑步比賽
- 選手各就各位,預備
- 開跑,選手有快有慢
- 最后一位選手過線,比賽結束
- 頒獎
countDownLatch
可以實現 所有線程都完成某個任務之后,再去執行其他的任務
跑步比賽代碼示例
public static void main(String[] args) throws InterruptedException {//指定參賽選手的個數(線程數)CountDownLatch countDownLatch = new CountDownLatch(10);//創建10個線程for (int i = 0; i < 10; i++) {Thread thread = new Thread(() -> {try {System.out.println("開跑....");//模擬業務執行時間,即比賽過程,休眠2秒TimeUnit.SECONDS.sleep(2);System.out.println("選手過線,到達終點...");//標記選手已經到達終點,當countDownLatch的計數到0時,表示所有選手都已到達終點,比賽結束countDownLatch.countDown();} catch (InterruptedException e) {throw new RuntimeException(e);}},"player" + i);//啟動線程thread.start();}TimeUnit.MILLISECONDS.sleep(10);System.out.println("比賽進行中....");//等待線程執行完畢,即等待比賽結束countDownLatch.await();//一直阻塞等待到計數器歸零,即所有選手都已經到達終點//頒獎System.out.println("比賽結束,開始頒獎");}
應用場景
線程安全的集合類
大部分的集合類都不是線程安全的
Vector
,Stack
,HashTable
,是線程安全的(不建議用),其他的集合類不是線程安全的
例如:在多線程環境下使用 ArrayList
集合類就會造成線程安全問題,代碼示例如下:
public static void main(String[] args) {//定義一個線程不安全的集合對象List<Integer> arrayList = new ArrayList<>();//創建10個線程,同時執行寫入和讀取操作for (int i = 0; i < 10; i++) {int num = i + 1;Thread thread = new Thread(() -> {//寫arrayList.add(num);//讀System.out.println(arrayList);});//啟動線程thread.start();}}
執行該代碼會出現異常:
針對這種線程不安全的集合類,我們使用工具類Collections.synchronizedList(new ArrayList)
把普通集合對象,轉換為線程安全的集合對象
Collections.synchronizedList(new ArrayList)
public static void main(String[] args) {//定義一個普通集合類對象List<Integer> arrayList = new ArrayList<>();//使用Collection工具類將普通集合類對象轉換為線程安全的集合類List<Integer> list = Collections.synchronizedList(arrayList);//創建10個線程,同時進行讀和寫的操作for (int i = 0; i < 10; i++) {int num = i + 1;Thread thread = new Thread(() -> {//讀list.add(num);//寫System.out.println(list);});//啟動線程thread.start();}}
源碼中,將普通集合類轉換成 線程安全的集合類后,就是在相關的修改數據的方法前面加了
synchronized
關鍵字
CopyOnWriteArrayList
(寫時復制)
寫時復制是指在
進行寫操作的時候,先復制一份新的集合,在新復制的集合中進行寫操作,寫操作完成后會將這個新復制的副本替換原來的舊集合,替換的過程需要加鎖;
寫時復制流程:
- ? 寫操作流程:
- 復制原集合 → 2. 修改副本→ 3. 加鎖替換原集合
- ? 讀操作流程:寫操作完成替換前,讀操作訪問舊集合;
? 寫操作完成替換后,后續讀操作訪問新集合
讀操作是否總是訪問舊集合?
- ? 不完全正確:
- 在寫操作完成替換前,讀操作訪問舊集合。
- 在寫操作完成替換后,后續讀操作訪問新集合。
- 關鍵點:讀操作不感知替換過程,直接訪問當前
volatile
數組引用。
在寫操作完成后,副本會替換原來的舊集合,即原引用指向副本,副本成為原始集合
底層使用
volatile
修飾的數組存儲數據,確保修改后的引用對其他線程立即可見,確保了內存可見性
寫時復制適用于 讀多寫少 的場景
適用場景與注意事項
場景 | 推薦使用 | 不推薦使用 |
---|---|---|
高頻讀、低頻寫(如監聽器列表) | ? | ? 高頻寫(如計數器) |
數據一致性要求寬松 | ? | ? 強一致性需求(如銀行轉賬) |
內存充足,可容忍臨時內存翻倍 | ? | ? 內存敏感場景 |
多線程環境下使用哈希表
HashMap
本身是線程不安全的,如果在多線程環境下使用哈希表應該使用:
Hashtable
ConcurrentHashMap
+++
Hashtable
:對所有方法都加了鎖,對性能有較大影響,會導致嚴重的效率問題ConcurrentHashMap
:并沒有對整個方法加鎖,而是對要操作的hash
桶加鎖,其他的桶不加鎖,理論上有多少個桶就可以支持多少個線程進行并發讀寫- 相較于
Hashtable
,ConcurrentHashmap
鎖的粒度更小,并發更高
+++
Hashtable
把所有的桶整體加了鎖,在put
操作時,把整個Hashtable
都給鎖住了,但真正操作的只有一個hash
桶
ConcurrentHashMap
只是對某一個桶進行加鎖,在put
操作時,修改哪個桶的數據,就對哪個桶加鎖
+++
在Java中,Hashtable
和ConcurrentHashMap
都是線程安全的哈希表實現,但它們在設計、性能和應用場景上有顯著差異。以下是它們的核心區別:
1. 線程安全實現方式
特性 | Hashtable | ConcurrentHashMap |
---|---|---|
鎖機制 | 使用全局鎖(所有方法用synchronized 修飾) | 桶鎖 + CAS(Java 8) |
鎖粒度 | 粗粒度(整個表被鎖) | 細粒度(僅鎖部分數據,如哈希桶) |
并發度 | 低(所有操作串行) | 高(多線程可并行操作不同桶) |
2. 性能對比
場景 | Hashtable | ConcurrentHashMap |
---|---|---|
讀操作 | 所有讀操作競爭同一把鎖,性能差 | 無鎖或細粒度鎖,讀性能高 |
寫操作 | 寫操作完全串行,高并發下性能差 | 多線程可同時寫不同桶,性能高 |
高并發場景 | 不適用(易成瓶頸) | 適用(設計為高并發優化) |
補充知識:HashMap的實現原理
put
一個對象進來時,先根據對象的HashCode
和數組長度進行求余,通過余數來確定對象放在數組的哪個下標中- 每個
hash
桶存的是具體對象的鏈表 - 初始化數組長度是16,中間還可能發生擴容,擴容的時候會對當前
hash
表中的所有元素重新hash
到新的hash
表中 - 發生擴容的條件:負載因子 > 0.75 時,默認負載因子是0.75
- 當數組長度大于8,同時鏈表長度大于64時,鏈表就會轉化為紅黑樹
+++
ConcurrentHashMap
的優化擴容機制
-
觸發條件:負載因子超過閾值0.75時觸發擴容
-
在擴容過程中,多個線程會協助擴容(協助遷移),多個線程協助把舊表中的數據遷移到新表中
-
在擴容過程中,任何正在執行插入、更新或刪除的線程檢測到擴容狀態,會主動參與遷移
-
遷移過程中,若線程執行查詢操作,可同時訪問舊表和新表;先查舊表,若未找到,則查新表
+++
1. 協作遷移機制
-
當線程執行寫操作時,若檢測到當前表正在擴容(
table
被標記為MOVED
):- 暫停當前寫操作,優先協助遷移數據
- 領取遷移任務:通過全局變量
transferIndex
分配遷移的桶區間 - 遷移完成:繼續執行原寫操作(寫入新表)
+++
2.讀操作的執行邏輯
- 無鎖設計:讀操作完全無鎖,通過
volatile
變量和內存屏障保證可見性 - 直接訪問數據:
- 若當前桶未遷移,直接從舊表讀取數據
- 若當前桶已遷移(被標記為
ForwardingNode
),則跳轉到新表讀取數據 - 若正在遷移中,可能同時訪問舊表和新表的已遷移部分
+++
多線程環境下,用哪個類來保證MAP的線程安全?
使用JUC包下的
ConcurrentHashMap
介紹下ConcurrentHashMap
, HashMap
,HashTable
的區別?
-
HashMap
是線程不安全的 -
HashTable
是線程安全的,對所有的操作都加了鎖,效率不高,不推薦使用HashTable
是使用synchronized
對所有操作加的鎖 -
ConcruuentHashMap
的鎖粒度比較小,并不是對整個HashMap
加鎖,而是對每一個數組的下標進行加鎖,也就意味著可以支持更大的并發量,從而提升性能
ConcureentHashMap
只對put
進行加鎖(對修改進行加鎖),對get
不進行加鎖ConcurrentHashMap
對擴容也進行了優化
ConcureentHashMap
的擴容機制:
1.擴容時把數組的容量增大到原來的2倍,并不是一次性能把MAP中的數據復制到新MAP中,而是只復制當前訪問的下標中的元素
2.這種操作會使兩個MAP
同時存在一段時間
3.當查詢時同時在兩個MAP
中查
4.刪除時在兩個MAP
中同時刪
5.寫入時,只往新的MAP
中寫
典型的以空間換時間的方法
每次調用get
put
方法時把舊MAP
中對應下標中的元素搬運到新MAP
中
++++
死鎖
- 一個線程,獲取一把鎖
- 在單線程環境中,如果使用不可重入鎖(Non-Reentrant Lock),同一線程多次嘗試獲取同一把鎖時,會導致線程阻塞自身,形成死鎖
- 在單線程環境中,使用可重入鎖(
ReentrantLock
),當同一個線程多次獲取同一把鎖時,不會造成死鎖現象
+++
- 兩個線程,獲取兩把鎖
- 兩個鎖對象 lock1 和 lock2 ,兩個線程A 和 B;
- 線程A先申請拿到lock1的鎖,再申請拿lock2的鎖;
- 線程B先申請拿到lock2的鎖,再申請拿lock1的鎖;
- 當兩個線程分別持有外層拿到的鎖,并嘗試獲取對方已經持有的鎖時,就會造成死鎖狀態
++++
- 多個線程,獲取多把鎖
? 場景:哲學家就餐問題
- 哲學家之間放一根筷子
- 先拿左手邊的筷子,再拿右手邊的筷子
- 吃完后放下兩根筷子,放回原位,等待下一次吃
- 哲學家就干兩件事,一個是吃,一個是等
這個模型在大多數的情況下運行良好,不會發生死鎖問題
但有一個極端情況,會發生死鎖狀態
面試題:
造成死鎖的原因:
-
互斥訪問線程1拿到了 鎖A,那么線程2就不能同時得到該鎖(互斥鎖)
-
不可搶占獲取到鎖的線程,除非自己主動釋放鎖,別的線程不能從他手里搶過來
-
保持與請求線程1已經獲取到了鎖A,還要在這個基礎上再去獲取鎖B
-
循環等待線程 1等待線程2釋放鎖,線程2等待線程3釋放鎖,線程3等待線程1釋放鎖…
以上四條是造成死鎖的必要條件,必須同時滿足,也就是說只要打破一條, 死鎖就不會形成
分析如何解決死鎖問題:
-
互斥訪問鎖的基本特性, 不能打破
-
不可搶占鎖的基本特性,不能打破
-
保持與請求和代碼的設計和實現相關,是可以打破的,只要規定一- 下獲取鎖的順序個
-
循環等待也可以被打破, 也是從設計的角度去合理制定獲取鎖的策略
策略:
-
給每一個筷子都編一 個號
-
讓每個哲學家都先拿編號小的筷子,再去拿編號大的筷子
-
吃一口面之后把筷子再放回去,讓別的哲學家再去獲取筷子
過程:
-
從1號到4號哲學家都拿到了最小編號的筷子
-
當5號哲學家拿編號小的筷子時,發現筷子已經被1號哲學家拿走了,那么他就要阻塞等待
-
由于5號哲學家不能拿編號小的筷子,也就意味著無法獲取到編號大的筷子
-
4號哲學家就可以拿到5號筷子,進行就餐,就完餐之后需要把所有的筷子放回原位
-
3 - 1號哲學家就可以拿起上-一個哲學家放下的編號大的筷子進行就餐
-
隨著1號哲學家就完餐放下了5號哲學家需要的編號小的1號筷子,這時就可以先拿編號小的筷子再拿編號:大的筷子進行就餐 解決了死鎖問題
+++
面試題:
-
你知道線程與進程的區別嗎?
-
線程的創建方式有幾種?
-
Runnable與Callable的區別?
-
JDK提供的線程池有幾種?
-
手動創建線程池時
ThreadPoolExcutor
有多少個參數,以及各參數的含義? -
線程池的拒絕策略有哪些?
-
請你描述一下線程池的 工作流程?
-
說一下什么是線程安全問題?
-
怎么解決線程安全問題?
-
Synchronized
和volatile
的作用與區別? -
JMM的特性?
-
Synchronized
鎖升級的過程? -
什么是偏向鎖,輕量級鎖,重量級鎖?
-
介紹一個CAS, 以及ABA問題?
-
ReentranLock
特性?和synchronized
的區別? -
JUC包下的工具類知道哪些?
-
線程安全的集合類有哪些?
-
用過
ConcurrnetHashMap
嗎?介紹一下? -
造成死鎖的原因?以及解決辦法?
++++