Java常見的鎖策略圖文詳解(附實例代碼)
常見的鎖策略
樂觀鎖 & 悲觀鎖
加鎖的時候預測這個鎖出現(xiàn)競爭性的可能性大還是???
預測這個鎖出現(xiàn)競爭可能性小 —— 樂觀鎖
預測這個鎖出現(xiàn)競爭可能性大 —— 悲觀鎖
輕量級鎖 & 重量級鎖
加鎖的開銷小 —— 輕量級鎖
加鎖的開銷大 —— 重量級鎖
自旋鎖 & 掛起等待鎖
遇到鎖沖突,先不著急阻塞,而是嘗試重新得到鎖。這個操作不涉及內(nèi)核態(tài),僅在用戶態(tài)方面進行,所以更加輕量 —— 自旋鎖
遇到鎖沖突,阻塞等待,等到合適的時機再拿鎖。涉及到系統(tǒng)的內(nèi)部調(diào)度,開銷大 —— 掛起等待鎖
公平鎖 & 非公平鎖
JVM約定了“先來后到” 是公平鎖(先請求得到鎖的線程會得到鎖)
“公平競爭,各憑本事”是不公平鎖(解鎖后,多個線程同時競爭一把鎖)
可重入鎖 & 不可重入鎖
一個線程,一把鎖,同時加鎖兩次
如果沒死鎖:可重入鎖;死鎖了:不可重入鎖
C++的std :: mutex 是不可重入鎖
可重入鎖也稱“可遞歸鎖”
synchronized是可重入鎖,以第一次加鎖為真,第二次加鎖跳過
普通互斥鎖 & 讀寫鎖
普通互斥鎖有
- 加鎖
- 解鎖
讀寫鎖有
- 加讀鎖
- 加寫鎖
- 解鎖
如果代碼中線程進行讀的操作,那可以使用“讀鎖”,寫的操作使用“寫鎖”
讀鎖與讀鎖之間不存在互斥 —— 讀的過程數(shù)據(jù)不會發(fā)生改變,只是讀
讀鎖與寫鎖之間存在互斥 —— 讀的過程中可能會被“寫”修改
寫鎖與寫鎖之間存在互斥 —— A寫的時候肯定不能被B修改
synchronized鎖
synchronized鎖是普通互斥鎖,是可重入鎖,是非公平鎖,有著自適應(yīng)的特點,根據(jù)內(nèi)部鎖競爭的激烈程度,自動調(diào)整內(nèi)部策略,感知不到,干預不了,但我們需要知道內(nèi)部策略細節(jié)
鎖升級

鎖消除
針對synchronized的一種編譯器優(yōu)化,在保證邏輯不變的情況下,如果編譯器確定你寫的代碼不需要鎖,但你手動加了鎖,會嘗試把鎖去掉。
但這個優(yōu)化十分保守,沒有十拿九穩(wěn)的情況下不會觸發(fā),我們也不能依賴編譯器優(yōu)化這個機制來寫代碼
鎖粗化
關(guān)聯(lián)到鎖的粒度,編譯器會根據(jù)實際情況來操作
如果鎖的粒度小,證明加鎖解鎖中間的邏輯少,雖然會留出時間給其他的線程來得到鎖,但也增加了線程競爭的次數(shù),也會增加阻塞的時間。
鎖的粒度雖然大,但是中間執(zhí)行的邏輯時間長,沒有反復加鎖解鎖的操作,其他線程也只能競爭一次。

CAS
Compare And Swap
解決線程安全,加鎖是一種普遍的策略,CAS是解決線程安全問題的另一種思路
顧名思義 比較與交換 —— 比較一個內(nèi)存與一個寄存器內(nèi)部的值,如果他兩的值相同,內(nèi)存會與寄存器另外一個值進行交換,交換后更關(guān)心內(nèi)存更新之后的值,就可以實現(xiàn)“鎖”的功能
這是CAS的執(zhí)行邏輯


上述針對CAS的執(zhí)行邏輯,能看出它并不是函數(shù),而是一條CPU指令,而且是原子的,那JVM如何運行的呢?
- CPU提供了CAS的執(zhí)行指令
- 操作系統(tǒng)對CAS指令進行了封裝并提供了API,通過API可以調(diào)用CAS機制
- JVM就可以通過操作系統(tǒng)調(diào)用API
- 我們的代碼就可以使用CAS
- Java對CAS進一步封裝,Java不建議你用CAS,但內(nèi)部像==原子類==、自旋鎖、synchronized,ConcurrentHashMap都使用了CAS
這又引申出了**原子類**
原子類

內(nèi)部沒有加鎖,但基于CAS實現(xiàn)了,所以不加鎖也能保證線程安全

可以實例化原子類的整數(shù)/字符…,并設(shè)定初始化的值

方法與注釋中的操作是一一對應(yīng)的,舉例getAndIncrement 是先得到舊值,再Increment(+1)
自旋鎖
synchronized內(nèi)部的自旋鎖,就是基于CAS實現(xiàn),這就是為什么自旋鎖輕量的原因(“無鎖”)

ABA問題
CAS的循環(huán)檢測,判定是否有其他線程穿插修改執(zhí)行的依據(jù)是循環(huán)判定是檢測的值是否有變化,但是這種檢測方式是不嚴謹?shù)模瑳]有變化不能代表沒有被修改過,可能某時刻的值是A,在下一時刻被修改了B,然后在第二次檢測之前又被修改回了A,這就是ABA問題
大部分時候ABA問題不會引起bug,但在某些極端的情況下是會的,就像買新手機一樣,你買的手機是“翻新機”,但翻新機不代表一定就有問題,只是說翻新機出現(xiàn)問題的概率比全新機更大的~
ABA的核心是:
在通過A的值相同判定是否有插隊,由于次數(shù)的修改有加也有減,就可能會出現(xiàn)ABA的問題,那么有一些解決方案:限制此處的操作(只能加或者只能減)、或者引入“版本號”概念,每次修改都讓“版本號”+1,再次使用判定的時候就不是用值來判定了,而是看版本號
那我們可以引申出以下問題??
- CAS是什么?
- CAS有什么應(yīng)用場景(CAS如何實現(xiàn)鎖)?
- CAS的ABA問題是怎么樣的
JUC包中常見的類
JUC——Java.util.Concurrent包
Callable接口
是泛型接口,描述一個任務(wù),唯一方法call()?
- 可以返回一個結(jié)果
- 可以拋出受檢異常
類似與Runnable,但與Runnable的void run() 不同的是Callable的 V call() throws Exception 有返回值,通常Callable任務(wù)會被交給FutureTask或ExecutorService.submit()來執(zhí)行
Callable<Integer> callable = new Callable<Integer>() {
int sum = 0;
@Override
public Integer call() throws Exception {
for (int i = 1; i <= 1000; i++) {
sum += i;
}
return sum;
}
};
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread t = new Thread(futureTask);
FutureTask & Callable 的聯(lián)系
Callable是一個接口,類似與Runnable,但有返回值,并且能拋出受檢異常
- 使用場景:需要執(zhí)行一個有返回值的任務(wù)時
FutureTask是一個類
實現(xiàn)了RunnableFuture 接口
?
public class FutureTask<V> implements RunnableFuture<V>?而RunnableFuture 又繼承了Runnable 和 Future
?
public interface RunnableFuture<V> extends Runnable, Future<V> { void run(); }?所以FutureTask既是一個任務(wù)
Runnable,也是一個容器Future?它內(nèi)部持有Callable,并在
run()方法中調(diào)用Callable的call()方法
他們的call()/ run()和線程的run()關(guān)系
?
Callable.call()?- 是用戶定義的邏輯,可以返回,可以拋出受檢異常
- 不能直接交給
Thread運行,因為它不是Runnable,Thread只能運行Runnable?
?
FutureTask.run()?- 是
Runnable的實現(xiàn) - 內(nèi)部會調(diào)用
Callable.call(),并把結(jié)果存起來,通過.get()得到
- 是
?
Thread.run()?- Thread本身的run()是空的,當你傳入一個
Runnable后它才會調(diào)用Runnable.run()? - 如果傳入的是
FutureTask,則會調(diào)用FutureTask.run(),再通過FutureTask.run()來調(diào)用Callable.call()?
- Thread本身的run()是空的,當你傳入一個
執(zhí)行鏈條:
- ?
Thread.run()->Runnable.run()? - ?
Thread.run()->FutureTask.run()->Callable.call()?
ExecutorService & Callable
?ExecutorService.submit(Callable)不會直接調(diào)用Callable.call(),而是間接調(diào)用
newTaskFor(task)
- 會把
Callable包裝成一個 FutureTask(RunnableFuture的實現(xiàn)類)。 - 這樣它既是一個
Runnable(能被線程池執(zhí)行),又是一個Future(能保存結(jié)果)。
- 會把
execute(ftask)
- 線程池把
FutureTask當作Runnable,交給工作線程運行。 - 線程執(zhí)行時,會調(diào)用
FutureTask.run()。
- 線程池把
FutureTask.run()
- 內(nèi)部調(diào)用
callable.call()來真正執(zhí)行你的邏輯,并把結(jié)果存起來。
- 內(nèi)部調(diào)用
執(zhí)行鏈條:
ExecutorService.submit(Callable)
- -> newTaskFor(Callable) (Callable -> FutureTask)
- -> execute(FutureTask)
- -> Worker(Thread) 執(zhí)行
- -> FutureTask.run()
- -> Callable.call()
對比Runnable,用Callable的優(yōu)勢
有返回值
- ?
Runnable.run()沒有返回值 - ?
Callable.call()有返回值,結(jié)合Future/FutureTask使用,就能拿到異步計算的結(jié)果
優(yōu)點:適合計算類任務(wù),例如“統(tǒng)計一批文件的大小”“并行計算求和”等
- ?
可以拋出受檢異常
- Runnable不能拋出受檢異常,有異常只能自己捕獲或者包裝成RuntimeException
- Callable可以拋出受檢異常,并保存到Future中,調(diào)用get()再拋出
優(yōu)點:讓異常處理邏輯更加自然,避免任務(wù)里硬編碼try-catch
和并發(fā)框架集成好
ExecutorService.submit(Callable task)會返回一個Future,可以用來
- 獲取結(jié)果:
future.get()? - 取消任務(wù):
future.cancel(true)? - 檢查狀態(tài):
future.isDone()?
- 獲取結(jié)果:
優(yōu)點:和Runnable相比,更合適在生產(chǎn)級并發(fā)框架應(yīng)用(線程池,F(xiàn)orkJoinPool)
支持函數(shù)式編程(lambda)
Callable是函數(shù)式接口,支持lambda表達式
?
Callable<Integer> task = () -> 42;?
優(yōu)點:代碼更加簡潔美觀
ReentrantLock可重入鎖
一個可重入互斥lock具有與使用synchronized方法和語句訪問的隱式監(jiān)視鎖相同的基本行為和語義,但具有擴展功能。
在上古時期的時候還沒有synchronized,當時java的加鎖就是用的ReentrantLock
支持trylock
嘗試加鎖,如果加鎖失敗,就直接返回(放棄),也支持指定超時時間
支持公平鎖

等待通知機制
- synchronized.wait()搭配notify(),只支持隨機喚醒或喚醒全部
- ReentrantLock搭配Condition類,喚醒功能更加豐富,能指定喚醒
與synchronized區(qū)別
相同點:
- synchronized 和 ReentrantLock 都是 Java 中提供的可重入鎖
不同點:
- 用法不同:synchronized 可以用來修飾普通方法、靜態(tài)方法和代碼塊;ReentrantLock 只能用于代碼塊;
- 獲取和釋放鎖的機制不同:進入synchronized 塊自動加鎖和執(zhí)行完后自動釋放鎖; ReentrantLock 需要顯示的手動加鎖和釋放鎖;
- 鎖類型不同:synchronized 是非公平鎖; ReentrantLock 默認為非公平鎖,也可以手動指定為公平鎖;
- 響應(yīng)中斷不同:synchronized 不能響應(yīng)中斷;ReentrantLock 可以響應(yīng)中斷,可用于解決死鎖的問題;
- 底層實現(xiàn)不同:synchronized 是 JVM 層面通過監(jiān)視器實現(xiàn)的;ReentrantLock 是基于 AQS 實現(xiàn)的。
總結(jié)
到此這篇關(guān)于Java常見鎖策略的文章就介紹到這了,更多相關(guān)Java常見鎖策略內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot實現(xiàn)緩存組件配置動態(tài)切換的步驟詳解
現(xiàn)在有多個springboot項目,但是不同的項目中使用的緩存組件是不一樣的,有的項目使用redis,有的項目使用ctgcache,現(xiàn)在需要用同一套代碼通過配置開關(guān),在不同的項目中切換這兩種緩存,本文介紹了SpringBoot實現(xiàn)緩存組件配置動態(tài)切換的步驟,需要的朋友可以參考下2024-07-07
SpringBoot?整合Security權(quán)限控制的初步配置
這篇文章主要為大家介紹了SpringBoot?整合Security權(quán)限控制的初步配置實例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-11-11
詳解SpringBoot2 使用Spring Session集群
這篇文章主要介紹了SpringBoot2 使用Spring Session集群,本文通過實例代碼給大家介紹的非常詳細,具有一定的參考借鑒價值 ,需要的朋友可以參考下2019-04-04
java中關(guān)于移位運算符的demo與總結(jié)(推薦)
下面小編就為大家?guī)硪黄猨ava中關(guān)于移位運算符的demo與總結(jié)(推薦)。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2016-05-05
VScode 打造完美java開發(fā)環(huán)境最新教程
這篇文章主要介紹了VScode 打造完美java開發(fā)環(huán)境最新教程,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-12-12
Java中的抽象工廠模式_動力節(jié)點Java學院整理
抽象工廠模式是工廠方法模式的升級版本,他用來創(chuàng)建一組相關(guān)或者相互依賴的對象。下面通過本文給大家分享Java中的抽象工廠模式,感興趣的朋友一起看看吧2017-08-08

