本篇文章給大家分享的是有關(guān)Java中的鎖種類介紹,小編覺得挺實(shí)用的,因此分享給大家學(xué)習(xí),希望大家閱讀完這篇文章后可以有所收獲,話不多說,跟著小編一起來看看吧。
朔城網(wǎng)站制作公司哪家好,找成都創(chuàng)新互聯(lián)公司!從網(wǎng)頁設(shè)計(jì)、網(wǎng)站建設(shè)、微信開發(fā)、APP開發(fā)、響應(yīng)式網(wǎng)站設(shè)計(jì)等網(wǎng)站項(xiàng)目制作,到程序開發(fā),運(yùn)營(yíng)維護(hù)。成都創(chuàng)新互聯(lián)公司成立與2013年到現(xiàn)在10年的時(shí)間,我們擁有了豐富的建站經(jīng)驗(yàn)和運(yùn)維經(jīng)驗(yàn),來保證我們的工作的順利進(jìn)行。專注于網(wǎng)站建設(shè)就選成都創(chuàng)新互聯(lián)公司。
樂觀鎖與悲觀鎖
鎖的一種宏觀分類是樂觀鎖與悲觀鎖。樂觀鎖與悲觀鎖并不是特定的指哪個(gè)鎖(Java 中也沒有那個(gè)具體鎖的實(shí)現(xiàn)名就叫樂觀鎖或悲觀鎖),而是在并發(fā)情況下兩種不同的策略。
樂觀鎖(Optimistic Lock)就是很樂觀,每次去拿數(shù)據(jù)的時(shí)候都認(rèn)為別人不會(huì)修改。所以不會(huì)上鎖。但是如果想要更新數(shù)據(jù),則會(huì)在更新之前檢查在讀取至更新這段時(shí)間別人有沒有修改過這個(gè)數(shù)據(jù)。如果修改過,則重新讀取,再次嘗試更新,循環(huán)上述步驟直到更新成功(當(dāng)然也允許更新失敗的線程放棄更新操作)。
悲觀鎖(Pessimistic Lock)就是很悲觀,每次去拿數(shù)據(jù)的時(shí)候都認(rèn)為別人會(huì)修改。所以每次都在拿數(shù)據(jù)的時(shí)候上鎖。
這樣別人拿數(shù)據(jù)的時(shí)候就會(huì)被擋住,直到悲觀鎖釋放,想獲取數(shù)據(jù)的線程再去獲取鎖,然后再獲取數(shù)據(jù)。
悲觀鎖阻塞事務(wù),樂觀鎖回滾重試,它們個(gè)有優(yōu)缺點(diǎn),沒有好壞之分,只有適應(yīng)場(chǎng)景的不同區(qū)別。比如:樂觀鎖適合用于寫比較少的情況下,即沖突真的很少發(fā)生的場(chǎng)景,這樣可以省去鎖的開銷,加大了系統(tǒng)的整個(gè)吞吐量。但是如果經(jīng)常產(chǎn)生沖突,上層應(yīng)用會(huì)不斷的進(jìn)行重試,這樣反而降低了性能,所以這種場(chǎng)景悲觀鎖比較合適。
總結(jié):樂觀鎖適合寫比較少,沖突很少發(fā)生的場(chǎng)景;而寫多,沖突多的場(chǎng)景適合使用悲觀鎖。
樂觀鎖的基礎(chǔ) --- CAS
在樂觀鎖的實(shí)現(xiàn)中,我們必須要了解的一個(gè)概念:CAS。
什么是 CAS 呢? Compare-and-Swap,即比較并替換,或者比較并設(shè)置。
比較:讀取到一個(gè)值 A,在將其更新為 B 之前,檢查原值是否為 A(未被其它線程修改過,這里忽略 ABA 問題)。
替換:如果是,更新 A 為 B,結(jié)束。如果不是,則不會(huì)更新。
上面兩個(gè)步驟都是原子操作,可以理解為瞬間完成,在 CPU 看來就是一步操作。
有了 CAS,就可以實(shí)現(xiàn)一個(gè)樂觀鎖:
public class OptimisticLockSample{ public void test(){ int data = 123; // 共享數(shù)據(jù) // 更新數(shù)據(jù)的線程會(huì)進(jìn)行如下操作 for (;;) { int oldData = data; int newData = doSomething(oldData); // 下面是模擬 CAS 更新操作,嘗試更新 data 的值 if (data == oldData) { // compare data = newData; // swap break; // finish } else { // 什么都不敢,循環(huán)重試 } } } /** * * 很明顯,test() 里面的代碼根本不是原子性的,只是展示了下 CAS 的流程。 * 因?yàn)檎嬲?CAS 利用了 CPU 指令。 * * */ }
在 Java 中也是通過 native 方法實(shí)現(xiàn)的 CAS。
public final class Unsafe { ... public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5); public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5); public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6); ... }
上面寫了一個(gè)簡(jiǎn)單直觀的樂觀鎖(確切的來說應(yīng)該是樂觀鎖流程)的實(shí)現(xiàn),它允許多個(gè)線程同時(shí)讀取(因?yàn)楦緵]有加鎖操作),如果更新數(shù)據(jù)的話,
有且僅有一個(gè)線程可以成功更新數(shù)據(jù),并導(dǎo)致其它線程需要回滾重試。CAS 利用 CPU 指令,從硬件層面保證了原子性,以達(dá)到類似于鎖的效果。
從樂觀鎖的整個(gè)流程中可以看出,并沒有加鎖和解鎖的操作,因此樂觀鎖策略也被稱作為無鎖編程。換句話說,樂觀鎖其實(shí)不是"鎖",
它僅僅是一個(gè)循環(huán)重試的 CAS 算法而已。
自旋鎖
synchronized 與 Lock interface
Java 中兩種實(shí)現(xiàn)加鎖的方式:一種是使用 synchronized 關(guān)鍵字,另一種是使用 Lock 接口的實(shí)現(xiàn)類。
在一篇文章中看到一個(gè)好的對(duì)比,非常形象,synchronized 關(guān)鍵字就像是自動(dòng)擋,可以滿足一切的駕駛需求。
但是如果你想要做更高級(jí)的操作,比如玩漂移或者各種高級(jí)的騷操作,那么就需要手動(dòng)擋,也就是 Lock 接口的實(shí)現(xiàn)類。
而 synchronized 在經(jīng)過 Java 每個(gè)版本的各種優(yōu)化后,效率也變得很高了。只是使用起來沒有 Lock 接口的實(shí)現(xiàn)類那么方便。
synchronized 鎖升級(jí)過程就是其優(yōu)化的核心:偏向鎖 -> 輕量級(jí)鎖 -> 重量級(jí)鎖
class Test{ private static final Object object = new Object(); public void test(){ synchronized(object) { // do something } } }
使用 synchronized 關(guān)鍵字鎖住某個(gè)代碼塊的時(shí)候,一開始鎖對(duì)象(就是上述代碼中的 object)并不是重量級(jí)鎖,而是偏向鎖。
偏向鎖的字面意思就是"偏向于第一個(gè)獲取它的線程"的鎖。線程執(zhí)行完同步代碼塊之后,并不會(huì)主動(dòng)釋放偏向鎖。當(dāng)?shù)诙蔚竭_(dá)同步代碼塊時(shí),線程會(huì)判斷此時(shí)持有鎖的線程是否就是自己(持有鎖的線程 ID 在對(duì)象頭里存儲(chǔ)),如果是則正常往下執(zhí)行。由于之前沒有釋放,這里就不需要重新加鎖,如果從頭到尾都是一個(gè)線程在使用鎖,很明顯偏向鎖幾乎沒有額外開銷,性能極高。
一旦有第二個(gè)線程加入鎖競(jìng)爭(zhēng),偏向鎖轉(zhuǎn)換為輕量級(jí)鎖(自旋鎖)。鎖競(jìng)爭(zhēng):如果多個(gè)線程輪流獲取一個(gè)鎖,但是每次獲取的時(shí)候都很順利,沒有發(fā)生阻塞,那么就不存在鎖競(jìng)爭(zhēng)。只有當(dāng)某線程獲取鎖的時(shí)候,發(fā)現(xiàn)鎖已經(jīng)被占用,需要等待其釋放,則說明發(fā)生了鎖競(jìng)爭(zhēng)。
在輕量級(jí)鎖狀態(tài)上繼續(xù)鎖競(jìng)爭(zhēng),沒有搶到鎖的線程進(jìn)行自旋操作,即在一個(gè)循環(huán)中不停判斷是否可以獲取鎖。獲取鎖的操作,就是通過 CAS 操作修改對(duì)象頭里的鎖標(biāo)志位。先比較當(dāng)前鎖標(biāo)志位是否為釋放狀態(tài),如果是,將其設(shè)置為鎖定狀態(tài),比較并設(shè)置是原子性操作,這個(gè)是 JVM 層面保證的。當(dāng)前線程就算持有了鎖,然后線程將當(dāng)前鎖的持有者信息改為自己。
假如我們獲取到鎖的線程操作時(shí)間很長(zhǎng),比如會(huì)進(jìn)行復(fù)雜的計(jì)算,數(shù)據(jù)量很大的網(wǎng)絡(luò)傳輸?shù)?;那么其它等待鎖的線程就會(huì)進(jìn)入長(zhǎng)時(shí)間的自旋操作,這個(gè)過程是非常耗資源的。其實(shí)這時(shí)候相當(dāng)于只有一個(gè)線程在有效地工作,其它的線程什么都干不了,在白白地消耗 CPU,這種現(xiàn)象叫做忙等。(busy-waiting)。所以如果多個(gè)線程使用獨(dú)占鎖,但是沒有發(fā)生鎖競(jìng)爭(zhēng),或者發(fā)生了很輕微的鎖競(jìng)爭(zhēng),那么synchronized 就是輕量級(jí)鎖,允許短時(shí)間的忙等現(xiàn)象。這是一種擇中的想法,短時(shí)間的忙等,換取線程在用戶態(tài)和內(nèi)核態(tài)之間切換的開銷。
顯然,忙等是有限度的(JVM 有一個(gè)計(jì)數(shù)器記錄自旋次數(shù),默認(rèn)允許循環(huán) 10 次,可以通過虛擬機(jī)參數(shù)更改)。如果鎖競(jìng)爭(zhēng)情況嚴(yán)重,
達(dá)到某個(gè)最大自旋次數(shù)的線程,會(huì)將輕量級(jí)鎖升級(jí)為重量級(jí)鎖(依然是通過 CAS 修改鎖標(biāo)志位,但不修改持有鎖的線程 ID)。當(dāng)后續(xù)線程嘗試獲取鎖時(shí),發(fā)現(xiàn)被占用的鎖是重量級(jí)鎖,則直接將自己掛起(而不是上面說的忙等,即不會(huì)自旋),等待釋放鎖的線程去喚醒。在 JDK1.6 之前, synchronized直接加重量級(jí)鎖,很明顯現(xiàn)在通過一系列的優(yōu)化過后,性能明顯得到了提升。
JVM 中,synchronized 鎖只能按照偏向鎖、輕量級(jí)鎖、重量級(jí)鎖的順序逐漸升級(jí)(也有把這個(gè)稱為鎖膨脹的過程),不允許降級(jí)。
可重入鎖(遞歸鎖)
可重入鎖的字面意思是"可以重新進(jìn)入的鎖",即允許同一個(gè)線程多次獲取同一把鎖。比如一個(gè)遞歸函數(shù)里有加鎖操作,遞歸函數(shù)里這個(gè)鎖會(huì)阻塞自己么?
如果不會(huì),那么這個(gè)鎖就叫可重入鎖(因?yàn)檫@個(gè)原因可重入鎖也叫做遞歸鎖)。
Java 中以 Reentrant 開頭命名的鎖都是可重入鎖,而且 JDK 提供的所有現(xiàn)成 Lock 的實(shí)現(xiàn)類,包括 synchronized 關(guān)鍵字鎖都是可重入的。
如果真的需要不可重入鎖,那么就需要自己去實(shí)現(xiàn)了,獲取去網(wǎng)上搜索一下,有很多,自己實(shí)現(xiàn)起來也很簡(jiǎn)單。
如果不是可重入鎖,在遞歸函數(shù)中就會(huì)造成死鎖,所以 Java 中的鎖基本都是可重入鎖,不可重入鎖的意義不是很大,我暫時(shí)沒有想到什么場(chǎng)景下會(huì)用到;
注意:有想到需要不可重入鎖場(chǎng)景的小伙伴們可以留言一起探討。
下圖展示一下 Lock 的相關(guān)實(shí)現(xiàn)類:
公平鎖和非公平鎖
如果多個(gè)線程申請(qǐng)一把公平鎖,那么獲得鎖的線程釋放鎖的時(shí)候,先申請(qǐng)的先得到,很公平。如果是非公平鎖,后申請(qǐng)的線程可能先獲得鎖,是隨機(jī)獲取還是其它方式,都是根據(jù)實(shí)現(xiàn)算法而定的。
對(duì) ReentrantLock 類來說,通過構(gòu)造函數(shù)可以指定該鎖是否是公平鎖,默認(rèn)是非公平鎖。因?yàn)樵诖蠖鄶?shù)情況下,非公平鎖的吞吐量比公平鎖的大,如果沒有特殊要求,優(yōu)先考慮使用非公平鎖。
而對(duì)于 synchronized 鎖而言,它只能是一種非公平鎖,沒有任何方式使其變成公平鎖。這也是 ReentrantLock 相對(duì)于 synchronized 鎖的一個(gè)優(yōu)點(diǎn),更加的靈活。
以下是 ReentrantLock 構(gòu)造器代碼:
/** * Creates an instance of {@code ReentrantLock} with the * given fairness policy. * * @param fair {@code true} if this lock should use a fair ordering policy */ public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
ReentrantLock 內(nèi)部實(shí)現(xiàn)了 FairSync 和 NonfairSync 兩個(gè)內(nèi)部類來實(shí)現(xiàn)公平鎖和非公平鎖。
可中斷鎖
字面意思是"可以響應(yīng)中斷的鎖"。
首先,我們需要理解的是什么是中斷。 Java 中并沒有提供任何可以直接中斷線程的方法,只提供了中斷機(jī)制。那么何為中斷機(jī)制呢?
線程 A 向線程 B 發(fā)出"請(qǐng)你停止運(yùn)行"的請(qǐng)求,就是調(diào)用 Thread.interrupt() 的方法(當(dāng)然線程 B 本身也可以給自己發(fā)送中斷請(qǐng)求,
即 Thread.currentThread().interrupt()),但線程 B 并不會(huì)立即停止運(yùn)行,而是自行選擇在合適的時(shí)間點(diǎn)以自己的方式響應(yīng)中斷,也可以直接忽略此中斷。也就是說,Java 的中斷不能直接終止線程,只是設(shè)置了狀態(tài)為響應(yīng)中斷的狀態(tài),需要被中斷的線程自己決定怎么處理。這就像在讀書的時(shí)候,老師在晚自習(xí)時(shí)叫學(xué)生自己復(fù)習(xí)功課,但學(xué)生是否復(fù)習(xí)功課,怎么復(fù)習(xí)功課則完全取決于學(xué)生自己。
回到鎖的分析上來,如果線程 A 持有鎖,線程 B 等待持獲取該鎖。由于線程 A 持有鎖的時(shí)間過長(zhǎng),線程 B 不想繼續(xù)等了,我們可以讓線程 B 中斷。
自己或者在別的線程里面中斷 B,這種就是 可中段鎖。
在 Java 中, synchronized 鎖是不可中斷鎖,而 Lock 的實(shí)現(xiàn)類都是 可中斷鎖。從而可以看出 JDK 自己實(shí)現(xiàn)的 Lock 鎖更加的靈活,這也就是有了 synchronized 鎖后,為什么還要實(shí)現(xiàn)那么些 Lock 的實(shí)現(xiàn)類。
Lock 接口的相關(guān)定義:
public interface Lock { void lock(); void lockInterruptibly() throws InterruptedException; boolean tryLock(); boolean tryLock(long time, TimeUnit unit) throws InterruptedException; void unlock(); Condition newCondition(); }
其中 lockInterruptibly 就是獲取可中斷鎖。
共享鎖
字面意思是多個(gè)線程可以共享一個(gè)鎖。一般用共享鎖都是在讀數(shù)據(jù)的時(shí)候,比如我們可以允許 10 個(gè)線程同時(shí)讀取一份共享數(shù)據(jù),這時(shí)候我們可以設(shè)置一個(gè)有 10 個(gè)憑證的共享鎖。
在 Java 中,也有具體的共享鎖實(shí)現(xiàn)類,比如 Semaphore。
互斥鎖
字面意思是線程之間互相排斥的鎖,也就是表明鎖只能被一個(gè)線程擁有。
在 Java 中, ReentrantLock、synchronized 鎖都是互斥鎖。
讀寫鎖
讀寫鎖其實(shí)是一對(duì)鎖,一個(gè)讀鎖(共享鎖)和一個(gè)寫鎖(互斥鎖、排他鎖)。
在 Java 中, ReadWriteLock 接口只規(guī)定了兩個(gè)方法,一個(gè)返回讀鎖,一個(gè)返回寫鎖。
public interface ReadWriteLock { /** * Returns the lock used for reading. * * @return the lock used for reading */ Lock readLock(); /** * Returns the lock used for writing. * * @return the lock used for writing */ Lock writeLock(); }
文章前面講過[樂觀鎖策略](#樂觀鎖的基礎(chǔ) --- CAS),所有線程可以隨時(shí)讀,僅在寫之前判斷值有沒有被更改。
讀寫鎖其實(shí)做的事情是一樣的,但是策略稍有不同。很多情況下,線程知道自己讀取數(shù)據(jù)后,是否是為了更改它。那么為何不在加鎖的時(shí)候直接明確。
這一點(diǎn)呢?如果我讀取值是為了更新它(SQL 的 for update 就是這個(gè)意思),那么加鎖的時(shí)候直接加寫鎖,我持有寫鎖的時(shí)候,別的線程。
無論是讀還是寫都需要等待;如果讀取數(shù)據(jù)僅僅是為了前端展示,那么加鎖時(shí)就明確加一個(gè)讀鎖,其它線程如果也要加讀鎖,不需要等待,可以直接獲取(讀鎖計(jì)數(shù)器加 1)。
雖然讀寫鎖感覺與樂觀鎖有點(diǎn)像,但是讀寫鎖是悲觀鎖策略。因?yàn)樽x寫鎖并沒有在更新前判斷值有沒有被修改過,而是在加鎖前決定應(yīng)該用讀鎖還是寫鎖。樂觀鎖特指無鎖編程。
JDK 內(nèi)部提供了一個(gè)唯一一個(gè) ReadWriteLock 接口實(shí)現(xiàn)類是 ReentrantReadWriteLock。通過名字可以看到該鎖提供了讀寫鎖,并且也是可重入鎖。
總結(jié)
Java 中使用的各種鎖基本都是悲觀鎖,那么 Java 中有樂觀鎖么?結(jié)果是肯定的,那就是 java.util.concurrent.atomic 下面的原子類都是通過樂觀鎖實(shí)現(xiàn)的。如下:
public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; }
通過上述源碼可以發(fā)現(xiàn),在一個(gè)循環(huán)里面不斷 CAS,直到成功為止。
參數(shù)介紹
-XX:-UseBiasedLocking=false 關(guān)閉偏向鎖 JDK1.6 -XX:+UseSpinning 開啟自旋鎖 -XX:PreBlockSpin=10 設(shè)置自旋次數(shù) JDK1.7 之后 去掉此參數(shù),由 JVM 控制
以上就是Java中的鎖種類介紹,小編相信有部分知識(shí)點(diǎn)可能是我們?nèi)粘9ぷ鲿?huì)見到或用到的。希望你能通過這篇文章學(xué)到更多知識(shí)。更多詳情敬請(qǐng)關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道。