系列文章目錄
前言
閱讀該文章之前要了解,鎖策略是為了解決什么問題
多線程帶來的的風(fēng)險-線程安全的問題的簡單實(shí)例-線程不安全的原因
提示:以下是本篇文章正文內(nèi)容,下面案例可供參考
一、六大"有鎖策略"
鎖沖突是指兩個線程對一個對象加鎖,產(chǎn)生了阻塞等待。
1. 樂觀鎖——悲觀鎖
樂觀鎖
-
假設(shè)數(shù)據(jù)一般情況下不會產(chǎn)生并發(fā)沖突,所以在數(shù)據(jù)進(jìn)行提交更新的時候,才會正式對數(shù)據(jù)是否產(chǎn)生并發(fā)沖突進(jìn)行檢測,如果發(fā)現(xiàn)并發(fā)沖突了,則讓返回用戶錯誤的信息,讓用戶決定如何去做。
-
預(yù)測接下來的鎖沖突不大(一般消耗的資源少,效率高點(diǎn))
悲觀鎖
-
總是假設(shè)最壞的情況,每次去拿數(shù)據(jù)的時候都認(rèn)為別人會修改,所以每次在拿數(shù)據(jù)的時候都會上鎖,這樣別人想拿這個數(shù)據(jù)就會阻塞直到它拿到鎖。
-
預(yù)測接下來的鎖沖突很大(一般消耗的資源多,效率低點(diǎn))
舉個例子
大學(xué)里的期末的最后一門考試結(jié)束后,當(dāng)天,輔導(dǎo)員就會通知假期就開始了,大家可以離校了。
-
同學(xué)A(樂觀鎖)認(rèn)為:反正,每次都是考試完,就可以直接走了,于是他就直接收拾行李,不等通知,直接當(dāng)時就回家了,出現(xiàn)意外再說。
-
同學(xué)B(悲觀鎖)認(rèn)為:萬一輔導(dǎo)員說這次放假延遲,大家都留校等領(lǐng)導(dǎo)通知,于是他就在宿舍一直等到輔導(dǎo)員通知,才開始收拾行李,出發(fā)回家。
此時,在B等待的時間里,A可能已經(jīng)到家了。(即A的回家效率高于B)
2. 輕量級鎖——重量級鎖
該文中出現(xiàn)的“樂觀鎖”“偏向鎖”“等都會在后面介紹,讀者不必先理解,可先大致有個印象
輕量級鎖
-
加鎖解鎖,過程更快更高效
-
輕量級鎖在Java中是一種樂觀鎖的方式,使用CAS(比較和交換)實(shí)現(xiàn),它是通過在對象頭中標(biāo)記為“偏向鎖”來實(shí)現(xiàn)的。當(dāng)一個線程獲得該偏向鎖時,它就可以直接訪問被鎖定的對象,而不用執(zhí)行任何額外的同步操作。如果有其他線程來訪問該對象,輕量級鎖就會自動退化為重量級鎖。
重量級鎖
-
加鎖解鎖,過程更慢更低效
-
重量級鎖在Java中是一種悲觀鎖的方式,使用互斥鎖(Mutex Lock)實(shí)現(xiàn),它需要操作系統(tǒng)的支持。當(dāng)有多個線程同時訪問一個共享資源時,重量級鎖會把其他線程阻塞,直到當(dāng)前線程執(zhí)行完畢,釋放鎖。這種方式的效率較低,因為線程的上下文切換和系統(tǒng)調(diào)用開銷較大。
總結(jié)
-
輕量級鎖適用于競爭不激烈的情況,而重量級鎖適用于競爭激烈的情況。在實(shí)際開發(fā)中,我們需要根據(jù)具體場景選擇合適的鎖機(jī)制,以達(dá)到最佳的性能。
-
同時,樂觀鎖可能是輕量級鎖,悲觀鎖可能是重量級鎖(不絕對)
3. 自旋鎖——掛起等待鎖
自旋鎖
- 一直占用CPU,不涉及線程阻塞和調(diào)度,持續(xù)不斷的請求鎖,一但鎖被釋放,就能立即得到,忙等
- 如果其他線程一直不釋放鎖,那它就一直持續(xù)消耗CPU資源(該代碼通常是純用戶態(tài),不會設(shè)置很長的時間)
掛起等待鎖
-
當(dāng)它發(fā)現(xiàn)沒有鎖的時候,就會進(jìn)入掛起等待狀態(tài)(掛機(jī)),掛起等待的時候是
不消耗 CPU的 -
它等待操作系統(tǒng)的通知喚醒,但是可能其他線程剛釋放了鎖,就被一直不斷請求的自旋鎖線程給槍走了,所以它只能繼續(xù)等待,具體拿到鎖的時機(jī),還得聽從操作系統(tǒng)的安排(該鎖一般是內(nèi)核機(jī)制,可能會等待較長的時間)
對照前文
-
自旋鎖是輕量級鎖的一種典型實(shí)現(xiàn)
-
掛起等待鎖是重量級鎖的一種典型實(shí)現(xiàn)
4. 互斥鎖——讀寫鎖
互斥鎖(例如:synchronized)
只有兩個操作:
-
進(jìn)入代碼塊,給該代碼塊加鎖。
-
出代碼塊,解鎖該帶代碼塊。
-
互斥鎖常用于保護(hù)共享數(shù)據(jù)結(jié)構(gòu)的訪問,如隊列、鏈表、散列表等。需要注意的是,互斥鎖使用不當(dāng)可能會帶來鎖競爭、死鎖等問題,
讀寫鎖(例如:ReentrantReadWriteLock)
-
給讀操作加鎖。(讀鎖,是一種共享鎖,可被多個線程同時擁有。當(dāng)讀鎖被占用時,其他讀鎖可以繼續(xù)被占用。共享性。)
-
給寫操作加鎖。(寫鎖,寫鎖是一種排他鎖,只能被一個線程占用,當(dāng)寫鎖被占用時,其他任何鎖都不能被占用。原子性。)
-
解鎖。
-
多個線程同時讀取一個變量,不會涉及到線程安全問題。讀寫鎖適用于對共享資源的讀操作頻繁,寫操作較少的情況,如高并發(fā)讀,比如緩存、數(shù)據(jù)維護(hù)等。讀寫鎖可以提高讀取效率,避免了互斥鎖的性能開銷。同時,寫操作的排他特性避免了并發(fā)寫操作對共享資源的影響,保證數(shù)據(jù)的正確性和一致性。
在讀鎖和寫鎖之間,約定:
-
讀鎖和讀鎖之間,不會鎖競爭,不會產(chǎn)生阻塞等待。(不會影響執(zhí)行速度)
-
寫鎖和寫鎖之間,有鎖競爭。(不會影響執(zhí)行速度)
-
讀鎖和寫鎖之間,有鎖競爭。(會影響速度,但是保證線程安全)
5. 可重入鎖——不可重入鎖
可重入鎖,又名遞歸鎖(例如:synchronized)
-
如果一個鎖,在一個線程中,連續(xù)加鎖兩次,不死鎖,就叫做可重入鎖,死鎖了,就叫不可重入鎖。即允許同一個線程多次獲取同一把鎖,而不會產(chǎn)生死鎖。
-
這種鎖能夠保證同一線程多次訪問同一資源時不會發(fā)生沖突。
-
Java里只要以Reentrant開頭命名的鎖都是可重入鎖,而且JDK提供的所有現(xiàn)成的Lock實(shí)現(xiàn)類,包括synchronized關(guān)鍵字鎖都是可重入的。
代碼示例
Object locker = new Object();
synchronized(locker) {
synchronized(locker) {
//連續(xù)加鎖兩次
}
}
//或者
//這也是兩次加鎖,針對this
class BlockingQueue {
synchronized void put(int elem) {
this.size();
}
synchronized int size() {}
}
不可重入鎖
-
同一線程第二次加鎖的時候, 會阻塞等待。直到第一次的鎖被釋放, 才能獲取到第二個鎖。 但是釋放第一個鎖也是由該線程來完成, 結(jié)果這個線程已經(jīng)阻塞了, 也就無法進(jìn)行解鎖操作.。這時候就會死鎖。
-
即在同一線程再次請求獲得該鎖時,會造成死鎖。因為該鎖只能被獲得一次,并且只有獲得鎖的線程才能釋放鎖。
-
Linux系統(tǒng)提供的 mutex是不可重入鎖.
6. 公平鎖——非公平鎖
公平鎖
-
是指多個線程按照申請鎖的順序來獲取鎖,即先到先得的策略。(公不公平是由自己對公平的定義決定,Java中定義先到先得為公平,synchronized為非公平鎖,它遵循等概率競爭規(guī)則)
-
公平鎖的優(yōu)點(diǎn)是可以避免饑餓現(xiàn)象,即線程在獲取鎖時會受到先來先服務(wù)的原則,公平性是保證鎖最大程度分配給等待時間最長的線程,缺點(diǎn)是其效率較低,因為需要保存大量的線程狀態(tài)。
非公平鎖
-
多個線程獲取鎖的順序是不確定的,有可能后申請鎖的線程先獲取到鎖,這種方式可能造成某些線程一直無法獲取到鎖。
-
在Java中,ReentrantLock默認(rèn)就是非公平鎖。與公平鎖相比,非公平鎖調(diào)度的效率要高,但是不公平的分配策略可能會導(dǎo)致某些線程一直無法獲取到鎖,從而產(chǎn)生“饑餓”的現(xiàn)象。
-
在Java中,ReentrantLock默認(rèn)是非公平鎖,可以通過它的構(gòu)造函數(shù)改為公平鎖。
二、Synchronized——ReentrantLock
Synchronized的特點(diǎn)(JDK1.8)
-
開始時是樂觀鎖,如果鎖沖突頻繁,就轉(zhuǎn)換為悲觀鎖。
-
開始時輕量級鎖,如果鎖被持有時間較長,就轉(zhuǎn)換為重量級鎖。
-
輕量級鎖大概率基于自旋實(shí)現(xiàn),重量級鎖大概率基于掛起等待實(shí)現(xiàn)。
-
不是讀寫鎖。
-
是可重入鎖。
-
是非公平鎖。
Synchronized的鎖升級策略
都是盡可能減少鎖帶來的的開銷
-
無鎖
-
偏向鎖(非必要不加鎖)
即線程對鎖有個標(biāo)記,沒有競爭就不加鎖,倘若有別的線程競爭,就立即加鎖,即高效又安全
-
自旋鎖 / 輕量級鎖(遇到了鎖競爭,但是目前線程較少,就讓它自旋一會,說不定很快就拿到了 )
-
重量級鎖(線程競爭激烈,多個線程都在自旋,大量占用cpu資源,直接升級鎖,調(diào)用系統(tǒng)內(nèi)核阻塞等待)
主流的JVM只能鎖升級,不能降級,不是實(shí)現(xiàn)不了,可能需要付出更大的代價,于是干脆就不降級了
ReentrantLock的特點(diǎn)
-
可重入:同一個線程可以多次獲取鎖,避免了死鎖的發(fā)生。
-
公平鎖和非公平鎖:ReentrantLock可以通過參數(shù)指定是公平鎖還是非公平鎖。
-
條件變量:ReentrantLock可以通過維護(hù)條件變量來實(shí)現(xiàn)線程間的協(xié)調(diào)。
-
中斷響應(yīng):ReentrantLock支持線程中斷,即在等待鎖的過程中,可以響應(yīng)中斷信號。
-
限時等待:ReentrantLock支持線程等待一定時間,如果在指定時間內(nèi)還未獲取到鎖,就會放棄等待。
Synchronized和ReentranLock對比
-
ReentranLock是可重入鎖,提供lock()和unlock()獨(dú)立方法(即需要手動釋放),來進(jìn)行加鎖解鎖,synchronized也是可重入鎖(基于代碼塊的方式來控制加鎖解鎖),它在第二次加鎖之前,會判定當(dāng)前鎖的擁有者是否是同一個線程,如果是,則直接放行,不必再加一次鎖
-
synchronized是非公平的,若想要公平,需要手動加個優(yōu)先級隊列來記錄順序。ReentrantLock提供公平和非公平兩種工作模式,默認(rèn)是非公平鎖, 在構(gòu)造方法中傳入true,開啟公平鎖。
-
synchronized搭配Object的wait和notify進(jìn)行等待喚醒,如果多個線程wait()同一個對象,notify()隨機(jī)喚醒一個。ReentrantLock需要搭配Condition這個類,這個類也能起到等待通知的作用,能夠精準(zhǔn)喚醒某個線程, 功能更強(qiáng)大。
-
synchronized是一個關(guān)鍵字, 是 JVM內(nèi)部實(shí)現(xiàn)的(大概率是基于 C++ 實(shí)現(xiàn)). ReentrantLock是標(biāo)準(zhǔn)庫的一個類, 在 JVM 外實(shí)現(xiàn)的(基于Java實(shí)現(xiàn))
-
synchronized在申請鎖失敗時, 會死等. ReentrantLock可以通過 trylock()的方式等待一段時間就放棄, 不會阻塞,而是返回false(讓用戶自己決定后續(xù)操作)。
三、鎖消除——鎖粗化
鎖消除
-
非必要不加鎖(不濫用synchronized)
-
編譯器+JVM就會會作出優(yōu)化,檢測當(dāng)前代碼是否是多線程執(zhí)行 / 是否有必要加鎖,如果沒必要,就自動把鎖去掉。
例如:StringBuilder和StringBuffer,后者帶鎖,但是如果單線程使用后者,就自動將后者優(yōu)化為前者。(該手段十分保守,只有保證消除是可靠的,才會啟動,寧愿什么也不做,也不愿意犯錯)
鎖粗化
-
鎖的粒度,synchronized代碼塊,包含代碼的多少(代碼越多,粒度越粗。代碼越少,粒度越細(xì))文章來源:http://www.zghlxwxcb.cn/news/detail-615012.html
-
一般寫代碼,多數(shù)情況下,希望粒度小一些。(串行執(zhí)行的代碼少,并發(fā)執(zhí)行的代碼多)但是如果某個場景,頻繁的加鎖/解鎖,此時編譯器就會把它優(yōu)化為一個更粗粒度的鎖。文章來源地址http://www.zghlxwxcb.cn/news/detail-615012.html
到了這里,關(guān)于【六大鎖策略-各種鎖的對比-Java中的Synchronized鎖和ReentrantLock鎖的特點(diǎn)分析-以及加鎖的合適時機(jī)】的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!