
樂觀鎖與悲觀鎖
樂觀鎖和悲觀鎖是一個(gè)宏觀上的分類,它并不是指特定的哪個(gè)鎖。而是在并發(fā)情況下兩種不同的策略。
樂觀鎖
就是很樂觀,每次去使用數(shù)據(jù)的時(shí)候都認(rèn)為不會(huì)有其他線程修改數(shù)據(jù),所以不會(huì)上鎖。在更新數(shù)據(jù)的時(shí)候,則會(huì)在更新之前先判斷有沒有別的線程更新了這個(gè)數(shù)據(jù),如果有,則重新讀取,再次嘗試更新,直到更新成功,或者報(bào)錯(cuò)哦放棄更新。如果沒有,則當(dāng)前線程將自己修改的數(shù)據(jù)成功寫入。
悲觀鎖
就是很悲觀,認(rèn)為每次去使用數(shù)據(jù)的時(shí)候一定有其他線程來修改數(shù)據(jù),所以在獲取數(shù)據(jù)的時(shí)候會(huì)先加鎖,確保數(shù)據(jù)不被修改。這樣其他線程拿數(shù)據(jù)的時(shí)候就會(huì)被擋住,直到鎖釋放。
樂觀鎖在 Java 中是通過無鎖編程實(shí)現(xiàn),通常采用 CAS 算法,在 Java 中,synchronized 和 Lock 的實(shí)現(xiàn)類都是悲觀鎖。
樂觀鎖基礎(chǔ)--CAS
CAS 全稱 Compare-and-Swa,即 比較并替換。
比較:讀取到一個(gè)值 A,在將其更新為 B 之前,檢查原來的值是否為 A(未被其他線程修改過)
替換:如果是 A,則把 A 更新為 B,結(jié)束,否則不會(huì)更新。
比較和替換都是原子操作,可以理解為瞬間完成,下面模仿寫一個(gè)樂觀鎖的邏輯偽代碼:
public void test(){
int data = 123; //數(shù)據(jù)
//更新數(shù)據(jù)的線程會(huì)進(jìn)行如下操作
while(true){
int oldData = data;
int newData = doSomething(oldData);
//模擬CAS操作
if(data==oldData){ //比較,檢查 data 有沒有被改變,沒有的話更新數(shù)據(jù),否則一直循環(huán)判斷比較
data = newData;
break;
}
}
}
Java 是通過 native 方法實(shí)現(xiàn)的 CAS。Android 中原子類就是使用 CAS 樂觀鎖,比如 AtomicInteger 等。
樂觀鎖的特點(diǎn)是回滾重試,悲觀鎖的特點(diǎn)是阻塞事務(wù),在寫操作比較少的情況下,即沖突很少發(fā)生的場(chǎng)景中,使用樂觀鎖可以省去鎖的開銷,加大了系統(tǒng)的吞吐量。但是如果在沖突經(jīng)常發(fā)生的情況下,樂觀鎖會(huì)不斷進(jìn)行重試,反而降低了性能,所以這時(shí)候悲觀鎖比較合適。
所以總結(jié):
● 悲觀鎖適合寫操作多的場(chǎng)景,先加鎖可以保證寫操作時(shí)數(shù)據(jù)正確。
● 樂觀鎖適合讀操作多的場(chǎng)景,不加鎖的特點(diǎn)能夠使其讀操作的性能大幅提升。
CAS 雖然高效,但是也存在三大問題:
- ABA 問題:即內(nèi)存值原來是 A,后來被修改成 B,然后又被修改成 A,那么在 CAS 檢查時(shí)會(huì)發(fā)現(xiàn)值沒有變化,但實(shí)際上是變化了的。解決版本是在變量前面加版本號(hào),每次更新時(shí)版本號(hào)加一,變化過程就變成了:
A-B-A >> 1A-2B-3A - 循環(huán)時(shí)間長開銷大:CAS 如果長時(shí)間不成功,會(huì)導(dǎo)致其一直自旋,給 CUP 帶來開銷
- 只能保證一個(gè)共享變量的原子操作:對(duì)多個(gè)共享變量操作時(shí),CAS 無法保證操作的原子性。
synchronized(讀:星隔來) 與 Lock interface
Java 的兩種加鎖方式:一種是使用 synchronized 關(guān)鍵字,一種是實(shí)現(xiàn) Lock 接口。
synchronized :
● 修飾普通方法
● 修飾靜態(tài)方法
● 修飾代碼塊
synchronized 的鎖升級(jí)過程
class Test{
private static final Object object = new Object();
public void test(){
synchronized(object) { // do something
}
}
}
當(dāng)使用 synchronized 鎖住某個(gè)代碼塊的時(shí)候,一開始鎖對(duì)象(即上面 object)并不是重量級(jí)鎖。而是偏向鎖。
偏向鎖的字面意思就是 “偏向于第一個(gè)獲取它的線程”的鎖,線程執(zhí)行完同步代碼塊之后,并不會(huì)主動(dòng)釋放偏向鎖。
當(dāng)?shù)诙蔚竭_(dá)同步代碼塊時(shí),線程會(huì)判斷此時(shí)持有鎖的線程是否就是自己,如果是則正常往下執(zhí)行。由于之前沒有釋放,這里就不需要再重新加鎖,如果重頭到尾都是一個(gè)線程在使用鎖,很明顯偏向鎖幾乎沒有額外開銷,性能極高。
一旦第二個(gè)線程加入來鎖競爭,偏向鎖會(huì)轉(zhuǎn)換為輕量級(jí)鎖。
鎖競爭:如果多個(gè)線程輪流獲取一個(gè)鎖,但是每次獲取的時(shí)候都很順利,沒有發(fā)生阻塞,就不存在鎖競爭,只有當(dāng)某個(gè)線程獲取鎖的時(shí)候,發(fā)現(xiàn)鎖已經(jīng)被占用,需要等待釋放,則說明發(fā)生了鎖競爭。
在輕量鎖狀態(tài)上如果繼續(xù)發(fā)生鎖競爭,沒有搶到鎖寫得線程會(huì)進(jìn)行自旋操作,即在一個(gè)循環(huán)中不停地判斷釋放可以獲取鎖。獲取鎖的操作,就是通過 CAS 操作修改對(duì)象頭里面的鎖標(biāo)志位。先比較當(dāng)前鎖標(biāo)記位是否為釋放狀態(tài),如果是,將其設(shè)置為鎖定狀態(tài),當(dāng)前線程就算持有了鎖,然后線程將當(dāng)前鎖的持有者信息改為自己。
當(dāng)獲取鎖的線程操作時(shí)間很長時(shí),比如進(jìn)行復(fù)雜計(jì)算,那么其他等待鎖的線程就會(huì)進(jìn)入長時(shí)間的自旋操作,其實(shí)這時(shí)候相當(dāng)于只有一個(gè)線程在工作,其他線程什么都做不了,這種現(xiàn)象稱為 忙等。
忙等 是有限度的,JVM 有一個(gè)計(jì)數(shù)器來記錄自旋次數(shù),默認(rèn)允許循環(huán) 10 次。
如果鎖競爭嚴(yán)重,當(dāng)某個(gè)線程的自旋次數(shù)達(dá)到最大時(shí),會(huì)將輕量級(jí)鎖升級(jí)為重量級(jí)鎖(修改方法依然是通過CAS修改鎖標(biāo)志位,但不修改持有鎖的線程 ID),當(dāng)后續(xù)線程嘗試獲取鎖時(shí),會(huì)發(fā)現(xiàn)被占用的鎖是重量級(jí)鎖,則直接將自己掛起,等待釋放鎖的線程去喚醒。
可重入鎖(遞歸鎖)
可重入鎖的意思是“可以重新進(jìn)入的鎖”,即允許同一個(gè)線程多次獲取同一把鎖。
比如在一個(gè)遞歸函數(shù)里有加鎖操作,那么遞歸函數(shù)會(huì)自己阻塞自己嗎?如果不會(huì),則這么鎖就是可重入鎖。
Java 中以 Reentrant 開頭命名的鎖都是可重入鎖,而且 JDK 提供的所有現(xiàn)成 Lock 的實(shí)現(xiàn)類,包括 synchronized 關(guān)鍵字鎖都是可重入的。
公平鎖和非公屏鎖
如果多線程申請(qǐng)一把公平鎖,那么獲得鎖的線程在釋放鎖的時(shí)候,先申請(qǐng)的先得到,很公平。
如果是非公平鎖,后申請(qǐng)的線程可能先獲取鎖,是隨機(jī)獲取還是其他方式,根據(jù)實(shí)現(xiàn)的算法而定。
如果沒有特殊要求,優(yōu)先考慮使用非公平鎖。而對(duì)于 synchronized 鎖而言,它只能是一種非公平鎖,沒有任何方式使其變成公平鎖。
可中斷鎖
意思是可以響應(yīng)中斷的鎖。
Java 中沒有提供任何可以直接中斷線程的方法,只提供中斷機(jī)制。
當(dāng)線程A 向線程B 發(fā)出停止運(yùn)行請(qǐng)求時(shí),就是調(diào)用 Thread.interrupt() 方法。但線程B 不會(huì)立即停止運(yùn)行,而是自行選擇在合適的時(shí)間點(diǎn)以自己的方式響應(yīng)中斷,也可以直接忽略此中斷。
也就是說,Java 不能直接中斷線程,只是設(shè)置了狀態(tài)為響應(yīng)中斷的狀態(tài),需要被中斷的線程自己決定怎么處理。
如果線程 A 持有鎖,線程 B 等待持獲取該鎖。由于線程 A 持有鎖的時(shí)間過長,線程 B 不想繼續(xù)等了,我們可以讓線程 B 中斷自己或者在別的線程里面中斷 B,這種就是 可中斷鎖。
synchronized 鎖是不可中斷鎖,而 Lock 的實(shí)現(xiàn)類都是 可中斷鎖。
共享鎖
字面意思是多個(gè)線程可以共享一個(gè)鎖。一般用共享鎖都是在讀數(shù)據(jù)的時(shí)候,比如我們可以允許 10 個(gè)線程同時(shí)讀取一份共享數(shù)據(jù),這時(shí)候我們可以設(shè)置一個(gè)有 10 個(gè)憑證的共享鎖。
互斥鎖
字面意思是線程之間互相排斥的鎖,也就是表明鎖只能被一個(gè)線程擁有。在 Java 中, ReentrantLock、synchronized 鎖都是互斥鎖。