前言
Java提供了種類豐富的鎖检疫,每種鎖因其特性的不同九杂,在適當(dāng)?shù)膱?chǎng)景下能夠展現(xiàn)出非常高的效率施禾。本文旨在對(duì)鎖相關(guān)源碼(本文中的源碼來自JDK 8)脚线、使用場(chǎng)景進(jìn)行舉例,為讀者介紹主流鎖的知識(shí)點(diǎn)拾积,以及不同的鎖的適用場(chǎng)景殉挽。
Java中往往是按照是否含有某一特性來定義鎖丰涉,我們通過特性將鎖進(jìn)行分組歸類,再使用對(duì)比的方式進(jìn)行介紹斯碌,幫助大家更快捷的理解相關(guān)知識(shí)一死。下面給出本文內(nèi)容的總體分類目錄:
1. 樂觀鎖 VS 悲觀鎖
樂觀鎖與悲觀鎖是一種廣義上的概念,體現(xiàn)了看待線程同步的不同角度傻唾。在Java和數(shù)據(jù)庫(kù)中都有此概念對(duì)應(yīng)的實(shí)際應(yīng)用投慈。
先說概念。對(duì)于同一個(gè)數(shù)據(jù)的并發(fā)操作冠骄,悲觀鎖認(rèn)為自己在使用數(shù)據(jù)的時(shí)候一定有別的線程來修改數(shù)據(jù)伪煤,因此在獲取數(shù)據(jù)的時(shí)候會(huì)先加鎖,確保數(shù)據(jù)不會(huì)被別的線程修改凛辣。Java中抱既,synchronized關(guān)鍵字和Lock的實(shí)現(xiàn)類都是悲觀鎖。
而樂觀鎖認(rèn)為自己在使用數(shù)據(jù)時(shí)不會(huì)有別的線程修改數(shù)據(jù)扁誓,所以不會(huì)添加鎖防泵,只是在更新數(shù)據(jù)的時(shí)候去判斷之前有沒有別的線程更新了這個(gè)數(shù)據(jù)。如果這個(gè)數(shù)據(jù)沒有被更新蝗敢,當(dāng)前線程將自己修改的數(shù)據(jù)成功寫入捷泞。如果數(shù)據(jù)已經(jīng)被其他線程更新,則根據(jù)不同的實(shí)現(xiàn)方式執(zhí)行不同的操作(例如報(bào)錯(cuò)或者自動(dòng)重試)寿谴。
樂觀鎖在Java中是通過使用無(wú)鎖編程來實(shí)現(xiàn)锁右,最常采用的是CAS算法,Java原子類中的遞增操作就通過CAS自旋實(shí)現(xiàn)的讶泰。
根據(jù)從上面的概念描述我們可以發(fā)現(xiàn):
- 悲觀鎖適合寫操作多的場(chǎng)景咏瑟,先加鎖可以保證寫操作時(shí)數(shù)據(jù)正確。
- 樂觀鎖適合讀操作多的場(chǎng)景峻厚,不加鎖的特點(diǎn)能夠使其讀操作的性能大幅提升响蕴。
我們來看下樂觀鎖和悲觀鎖的調(diào)用方式示例:
// ------------------------- 悲觀鎖的調(diào)用方式 -------------------------
// synchronized
public synchronized void testMethod() {
// 操作同步資源
}
// ReentrantLock
private ReentrantLock lock = new ReentrantLock(); // 需要保證多個(gè)線程使用的是同一個(gè)鎖
public void modifyPublicResources() {
lock.lock();
// 操作同步資源
lock.unlock();
}
// ------------------------- 樂觀鎖的調(diào)用方式 -------------------------
private AtomicInteger atomicInteger = new AtomicInteger(); // 需要保證多個(gè)線程使用的是同一個(gè)AtomicInteger
atomicInteger.incrementAndGet(); //執(zhí)行自增1
通過調(diào)用方式示例,我們可以發(fā)現(xiàn)悲觀鎖基本都是在顯式的鎖定之后再操作同步資源惠桃,而樂觀鎖則直接去操作同步資源浦夷。那么,為何樂觀鎖能夠做到不鎖定同步資源也可以正確的實(shí)現(xiàn)線程同步呢辜王?我們通過介紹樂觀鎖的主要實(shí)現(xiàn)方式 “CAS” 的技術(shù)原理來為大家解惑劈狐。
CAS
CAS全稱 Compare And Swap(比較與交換),是一種無(wú)鎖算法呐馆。在不使用鎖(沒有線程被阻塞)的情況下實(shí)現(xiàn)多線程之間的變量同步肥缔。
java.util.concurrent包中的原子類就是通過CAS來實(shí)現(xiàn)了樂觀鎖。
CAS算法涉及到三個(gè)操作數(shù):
- 需要讀寫的內(nèi)存值 V续膳。
- 進(jìn)行比較的值 A。
- 要寫入的新值 B。
之前提到j(luò)ava.util.concurrent包中的原子類,就是通過CAS來實(shí)現(xiàn)了樂觀鎖,那么我們進(jìn)入原子類AtomicInteger的源碼悲伶,看一下AtomicInteger的定義:
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
根據(jù)定義我們可以看出各屬性的作用:
- unsafe: 獲取并操作內(nèi)存的數(shù)據(jù)舆声。
- valueOffset: 存儲(chǔ)value在AtomicInteger中的偏移量。
- value: 存儲(chǔ)AtomicInteger的int值俩功,該屬性需要借助volatile關(guān)鍵字保證其在線程間是可見的蔓罚。
接下來,我們查看AtomicInteger的自增函數(shù)incrementAndGet()的源碼時(shí)噪沙,發(fā)現(xiàn)自增函數(shù)底層調(diào)用的是unsafe.getAndAddInt()。但是由于JDK本身只有Unsafe.class,只通過class文件中的參數(shù)名檩帐,并不能很好的了解方法的作用,所以我們通過OpenJDK 8 來查看Unsafe的源碼:
// ------------------------- JDK 8 -------------------------
// AtomicInteger 自增方法
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
// Unsafe.class
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;
}
// ------------------------- OpenJDK 8 -------------------------
// Unsafe.java
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
根據(jù)OpenJDK 8的源碼我們可以看出,getAndAddInt()循環(huán)獲取給定對(duì)象o中的偏移量處的值v煞躬,然后判斷內(nèi)存值是否等于v恩沛。如果相等則將內(nèi)存值設(shè)置為 v + delta部逮,否則返回false傅事,繼續(xù)循環(huán)進(jìn)行重試,直到設(shè)置成功才能退出循環(huán)峡扩,并且將舊值返回蹭越。整個(gè)“比較+更新”操作封裝在compareAndSwapInt()中,在JNI里是借助于一個(gè)CPU指令完成的教届,屬于原子操作般又,可以保證多個(gè)線程都能夠看到同一個(gè)變量的修改值。
后續(xù)JDK通過CPU的cmpxchg指令巍佑,去比較寄存器中的 A 和 內(nèi)存中的值 V。如果相等寄悯,就把要寫入的新值 B 存入內(nèi)存中萤衰。如果不相等,就將內(nèi)存值 V 賦值給寄存器中的值 A猜旬。然后通過Java代碼中的while循環(huán)再次調(diào)用cmpxchg指令進(jìn)行重試脆栋,直到設(shè)置成功為止。
CAS雖然很高效洒擦,但是它也存在三大問題椿争,這里也簡(jiǎn)單說一下:
-
ABA問題 CAS需要在操作值的時(shí)候檢查內(nèi)存值是否發(fā)生變化,沒有發(fā)生變化才會(huì)更新內(nèi)存值熟嫩。但是如果內(nèi)存值原來是A秦踪,后來變成了B,然后又變成了A,那么CAS進(jìn)行檢查時(shí)會(huì)發(fā)現(xiàn)值沒有發(fā)生變化椅邓,但是實(shí)際上是有變化的柠逞。ABA問題的解決思路就是在變量前面添加版本號(hào),每次變量更新的時(shí)候都把版本號(hào)加一景馁,這樣變化過程就從“A-B-A”變成了“1A-2B-3A”板壮。
JDK從1.5開始提供了AtomicStampedReference類來解決ABA問題,具體操作封裝在compareAndSet()中合住。compareAndSet()首先檢查當(dāng)前引用和當(dāng)前標(biāo)志與預(yù)期引用和預(yù)期標(biāo)志是否相等绰精,如果都相等,則以原子方式將引用值和標(biāo)志的值設(shè)置為給定的更新值透葛。 - 循環(huán)時(shí)間長(zhǎng)開銷大 CAS操作如果長(zhǎng)時(shí)間不成功笨使,會(huì)導(dǎo)致其一直自旋,給CPU帶來非常大的開銷获洲。
- 只能保證一個(gè)共享變量的原子操作 對(duì)一個(gè)共享變量執(zhí)行操作時(shí)阱表,CAS能夠保證原子操作,但是對(duì)多個(gè)共享變量操作時(shí)贡珊,CAS是無(wú)法保證操作的原子性的最爬。
Java從1.5開始JDK提供了AtomicReference類來保證引用對(duì)象之間的原子性,可以把多個(gè)變量放在一個(gè)對(duì)象里來進(jìn)行CAS操作门岔。
2. 自旋鎖 VS 適應(yīng)性自旋鎖
在介紹自旋鎖前爱致,我們需要介紹一些前提知識(shí)來幫助大家明白自旋鎖的概念。
阻塞或喚醒一個(gè)Java線程需要操作系統(tǒng)切換CPU狀態(tài)來完成寒随,這種狀態(tài)轉(zhuǎn)換需要耗費(fèi)處理器時(shí)間糠悯。如果同步代碼塊中的內(nèi)容過于簡(jiǎn)單,狀態(tài)轉(zhuǎn)換消耗的時(shí)間有可能比用戶代碼執(zhí)行的時(shí)間還要長(zhǎng)妻往。
在許多場(chǎng)景中互艾,同步資源的鎖定時(shí)間很短,為了這一小段時(shí)間去切換線程讯泣,線程掛起和恢復(fù)現(xiàn)場(chǎng)的花費(fèi)可能會(huì)讓系統(tǒng)得不償失纫普。如果物理機(jī)器有多個(gè)處理器,能夠讓兩個(gè)或以上的線程同時(shí)并行執(zhí)行好渠,我們就可以讓后面那個(gè)請(qǐng)求鎖的線程不放棄CPU的執(zhí)行時(shí)間昨稼,看看持有鎖的線程是否很快就會(huì)釋放鎖。
而為了讓當(dāng)前線程“稍等一下”拳锚,我們需讓當(dāng)前線程進(jìn)行自旋假栓,如果在自旋完成后前面鎖定同步資源的線程已經(jīng)釋放了鎖,那么當(dāng)前線程就可以不必阻塞而是直接獲取同步資源霍掺,從而避免切換線程的開銷匾荆。這就是自旋鎖拌蜘。
自旋鎖本身是有缺點(diǎn)的,它不能代替阻塞棋凳。自旋等待雖然避免了線程切換的開銷拦坠,但它要占用處理器時(shí)間。如果鎖被占用的時(shí)間很短剩岳,自旋等待的效果就會(huì)非常好贞滨。反之,如果鎖被占用的時(shí)間很長(zhǎng)拍棕,那么自旋的線程只會(huì)白浪費(fèi)處理器資源晓铆。所以,自旋等待的時(shí)間必須要有一定的限度绰播,如果自旋超過了限定次數(shù)(默認(rèn)是10次骄噪,可以使用-XX:PreBlockSpin來更改)沒有成功獲得鎖,就應(yīng)當(dāng)掛起線程蠢箩。
自旋鎖的實(shí)現(xiàn)原理同樣也是CAS链蕊,AtomicInteger中調(diào)用unsafe進(jìn)行自增操作的源碼中的do-while循環(huán)就是一個(gè)自旋操作,如果修改數(shù)值失敗則通過循環(huán)來執(zhí)行自旋谬泌,直至修改成功滔韵。
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
自旋鎖在JDK1.4.2中引入,使用-XX:+UseSpinning來開啟掌实。JDK 6中變?yōu)槟J(rèn)開啟陪蜻,并且引入了自適應(yīng)的自旋鎖(適應(yīng)性自旋鎖)。
自適應(yīng)意味著自旋的時(shí)間(次數(shù))不再固定贱鼻,而是由前一次在同一個(gè)鎖上的自旋時(shí)間及鎖的擁有者的狀態(tài)來決定宴卖。如果在同一個(gè)鎖對(duì)象上,自旋等待剛剛成功獲得過鎖邻悬,并且持有鎖的線程正在運(yùn)行中症昏,那么虛擬機(jī)就會(huì)認(rèn)為這次自旋也是很有可能再次成功,進(jìn)而它將允許自旋等待持續(xù)相對(duì)更長(zhǎng)的時(shí)間父丰。如果對(duì)于某個(gè)鎖齿兔,自旋很少成功獲得過,那在以后嘗試獲取這個(gè)鎖時(shí)將可能省略掉自旋過程础米,直接阻塞線程,避免浪費(fèi)處理器資源添诉。
在自旋鎖中 另有三種常見的鎖形式:TicketLock屁桑、CLHlock和MCSlock,本文中僅做名詞介紹栏赴,不做深入講解蘑斧,感興趣的同學(xué)可以自行查閱相關(guān)資料。
3. 無(wú)鎖 VS 偏向鎖 VS 輕量級(jí)鎖 VS 重量級(jí)鎖
這四種鎖是指鎖的狀態(tài),專門針對(duì)synchronized的竖瘾。在介紹這四種鎖狀態(tài)之前還需要介紹一些額外的知識(shí)沟突。
首先為什么Synchronized能實(shí)現(xiàn)線程同步?
在回答這個(gè)問題之前我們需要了解兩個(gè)重要的概念:“Java對(duì)象頭”捕传、“Monitor”惠拭。
Java對(duì)象頭
synchronized是悲觀鎖,在操作同步資源之前需要給同步資源先加鎖庸论,這把鎖就是存在Java對(duì)象頭里的职辅,而Java對(duì)象頭又是什么呢?
我們以Hotspot虛擬機(jī)為例聂示,Hotspot的對(duì)象頭主要包括兩部分?jǐn)?shù)據(jù):Mark Word(標(biāo)記字段)域携、Klass Pointer(類型指針)。
Mark Word: 默認(rèn)存儲(chǔ)對(duì)象的HashCode鱼喉,分代年齡和鎖標(biāo)志位信息秀鞭。這些信息都是與對(duì)象自身定義無(wú)關(guān)的數(shù)據(jù),所以Mark Word被設(shè)計(jì)成一個(gè)非固定的數(shù)據(jù)結(jié)構(gòu)以便在極小的空間內(nèi)存存儲(chǔ)盡量多的數(shù)據(jù)扛禽。它會(huì)根據(jù)對(duì)象的狀態(tài)復(fù)用自己的存儲(chǔ)空間锋边,也就是說在運(yùn)行期間Mark Word里存儲(chǔ)的數(shù)據(jù)會(huì)隨著鎖標(biāo)志位的變化而變化。
Klass Point: 對(duì)象指向它的類元數(shù)據(jù)的指針旋圆,虛擬機(jī)通過這個(gè)指針來確定這個(gè)對(duì)象是哪個(gè)類的實(shí)例宠默。
Monitor
Monitor可以理解為一個(gè)同步工具或一種同步機(jī)制,通常被描述為一個(gè)對(duì)象灵巧。每一個(gè)Java對(duì)象就有一把看不見的鎖搀矫,稱為內(nèi)部鎖或者M(jìn)onitor鎖。
Monitor是線程私有的數(shù)據(jù)結(jié)構(gòu)刻肄,每一個(gè)線程都有一個(gè)可用monitor record列表瓤球,同時(shí)還有一個(gè)全局的可用列表。每一個(gè)被鎖住的對(duì)象都會(huì)和一個(gè)monitor關(guān)聯(lián)敏弃,同時(shí)monitor中有一個(gè)Owner字段存放擁有該鎖的線程的唯一標(biāo)識(shí)卦羡,表示該鎖被這個(gè)線程占用。
現(xiàn)在話題回到synchronized麦到,synchronized通過Monitor來實(shí)現(xiàn)線程同步绿饵,Monitor是依賴于底層的操作系統(tǒng)的Mutex Lock(互斥鎖)來實(shí)現(xiàn)的線程同步。
如同我們?cè)谧孕i中提到的“阻塞或喚醒一個(gè)Java線程需要操作系統(tǒng)切換CPU狀態(tài)來完成瓶颠,這種狀態(tài)轉(zhuǎn)換需要耗費(fèi)處理器時(shí)間拟赊。如果同步代碼塊中的內(nèi)容過于簡(jiǎn)單,狀態(tài)轉(zhuǎn)換消耗的時(shí)間有可能比用戶代碼執(zhí)行的時(shí)間還要長(zhǎng)”粹淋。這種方式就是synchronized最初實(shí)現(xiàn)同步的方式吸祟,這就是JDK 6之前synchronized效率低的原因瑟慈。這種依賴于操作系統(tǒng)Mutex Lock所實(shí)現(xiàn)的鎖我們稱之為“重量級(jí)鎖”,JDK 6中為了減少獲得鎖和釋放鎖帶來的性能消耗屋匕,引入了“偏向鎖”和“輕量級(jí)鎖”葛碧。
所以目前鎖一共有4種狀態(tài)进泼,級(jí)別從低到高依次是:無(wú)鎖刷袍、偏向鎖雷酪、輕量級(jí)鎖和重量級(jí)鎖墩弯。鎖狀態(tài)只能升級(jí)不能降級(jí)引矩。
通過上面的介紹值漫,我們對(duì)synchronized的加鎖機(jī)制以及相關(guān)知識(shí)有了一個(gè)了解晚吞,那么下面我們給出四種鎖狀態(tài)對(duì)應(yīng)的的Mark Word內(nèi)容,然后再分別講解四種鎖狀態(tài)的思路以及特點(diǎn):
鎖狀態(tài) | 存儲(chǔ)內(nèi)容 | 存儲(chǔ)內(nèi)容 |
---|---|---|
無(wú)鎖 | 對(duì)象的hashCode祷愉、對(duì)象分代年齡髓窜、是否是偏向鎖(0) | 01 |
偏向鎖 | 偏向線程ID帆阳、偏向時(shí)間戳资锰、對(duì)象分代年齡、是否是偏向鎖(1) | 01 |
輕量級(jí)鎖 | 指向棧中鎖記錄的指針 | 00 |
重量級(jí)鎖 | 指向互斥量(重量級(jí)鎖)的指針 | 10 |
無(wú)鎖
無(wú)鎖沒有對(duì)資源進(jìn)行鎖定,所有的線程都能訪問并修改同一個(gè)資源焕议,但同時(shí)只有一個(gè)線程能修改成功。
無(wú)鎖的特點(diǎn)就是修改操作在循環(huán)內(nèi)進(jìn)行馋记,線程會(huì)不斷的嘗試修改共享資源号坡。如果沒有沖突就修改成功并退出,否則就會(huì)繼續(xù)循環(huán)嘗試梯醒。如果有多個(gè)線程修改同一個(gè)值宽堆,必定會(huì)有一個(gè)線程能修改成功,而其他修改失敗的線程會(huì)不斷重試直到修改成功茸习。上面我們介紹的CAS原理及應(yīng)用即是無(wú)鎖的實(shí)現(xiàn)畜隶。無(wú)鎖無(wú)法全面代替有鎖,但無(wú)鎖在某些場(chǎng)合下的性能是非常高的号胚。
偏向鎖
偏向鎖是指一段同步代碼一直被一個(gè)線程所訪問籽慢,那么該線程會(huì)自動(dòng)獲取鎖,降低獲取鎖的代價(jià)猫胁。
在大多數(shù)情況下箱亿,鎖總是由同一線程多次獲得,不存在多線程競(jìng)爭(zhēng)弃秆,所以出現(xiàn)了偏向鎖届惋。其目標(biāo)就是在只有一個(gè)線程執(zhí)行同步代碼塊時(shí)能夠提高性能。
當(dāng)一個(gè)線程訪問同步代碼塊并獲取鎖時(shí)菠赚,會(huì)在Mark Word里存儲(chǔ)鎖偏向的線程ID脑豹。在線程進(jìn)入和退出同步塊時(shí)不再通過CAS操作來加鎖和解鎖,而是檢測(cè)Mark Word里是否存儲(chǔ)著指向當(dāng)前線程的偏向鎖衡查。引入偏向鎖是為了在無(wú)多線程競(jìng)爭(zhēng)的情況下盡量減少不必要的輕量級(jí)鎖執(zhí)行路徑瘩欺,因?yàn)檩p量級(jí)鎖的獲取及釋放依賴多次CAS原子指令,而偏向鎖只需要在置換ThreadID的時(shí)候依賴一次CAS原子指令即可。
偏向鎖只有遇到其他線程嘗試競(jìng)爭(zhēng)偏向鎖時(shí)俱饿,持有偏向鎖的線程才會(huì)釋放鎖歌粥,線程不會(huì)主動(dòng)釋放偏向鎖。偏向鎖的撤銷拍埠,需要等待全局安全點(diǎn)(在這個(gè)時(shí)間點(diǎn)上沒有字節(jié)碼正在執(zhí)行)阁吝,它會(huì)首先暫停擁有偏向鎖的線程,判斷鎖對(duì)象是否處于被鎖定狀態(tài)械拍。撤銷偏向鎖后恢復(fù)到無(wú)鎖(標(biāo)志位為“01”)或輕量級(jí)鎖(標(biāo)志位為“00”)的狀態(tài)。
偏向鎖在JDK 6及以后的JVM里是默認(rèn)啟用的装盯】缆牵可以通過JVM參數(shù)關(guān)閉偏向鎖:-XX:-UseBiasedLocking=false,關(guān)閉之后程序默認(rèn)會(huì)進(jìn)入輕量級(jí)鎖狀態(tài)埂奈。
輕量級(jí)鎖
是指當(dāng)鎖是偏向鎖的時(shí)候迄损,被另外的線程所訪問,偏向鎖就會(huì)升級(jí)為輕量級(jí)鎖账磺,其他線程會(huì)通過自旋的形式嘗試獲取鎖芹敌,不會(huì)阻塞,從而提高性能垮抗。
在代碼進(jìn)入同步塊的時(shí)候氏捞,如果同步對(duì)象鎖狀態(tài)為無(wú)鎖狀態(tài)(鎖標(biāo)志位為“01”狀態(tài),是否為偏向鎖為“0”)冒版,虛擬機(jī)首先將在當(dāng)前線程的棧幀中建立一個(gè)名為鎖記錄(Lock Record)的空間液茎,用于存儲(chǔ)鎖對(duì)象目前的Mark Word的拷貝,然后拷貝對(duì)象頭中的Mark Word復(fù)制到鎖記錄中辞嗡。
拷貝成功后捆等,虛擬機(jī)將使用CAS操作嘗試將對(duì)象的Mark Word更新為指向Lock Record的指針,并將Lock Record里的owner指針指向?qū)ο蟮腗ark Word续室。
如果這個(gè)更新動(dòng)作成功了栋烤,那么這個(gè)線程就擁有了該對(duì)象的鎖,并且對(duì)象Mark Word的鎖標(biāo)志位設(shè)置為“00”挺狰,表示此對(duì)象處于輕量級(jí)鎖定狀態(tài)明郭。
如果輕量級(jí)鎖的更新操作失敗了,虛擬機(jī)首先會(huì)檢查對(duì)象的Mark Word是否指向當(dāng)前線程的棧幀她渴,如果是就說明當(dāng)前線程已經(jīng)擁有了這個(gè)對(duì)象的鎖达址,那就可以直接進(jìn)入同步塊繼續(xù)執(zhí)行,否則說明多個(gè)線程競(jìng)爭(zhēng)鎖趁耗。
若當(dāng)前只有一個(gè)等待線程沉唠,則該線程通過自旋進(jìn)行等待。但是當(dāng)自旋超過一定的次數(shù)苛败,或者一個(gè)線程在持有鎖满葛,一個(gè)在自旋径簿,又有第三個(gè)來訪時(shí),輕量級(jí)鎖升級(jí)為重量級(jí)鎖嘀韧。
重量級(jí)鎖
升級(jí)為重量級(jí)鎖時(shí)篇亭,鎖標(biāo)志的狀態(tài)值變?yōu)椤?0”,此時(shí)Mark Word中存儲(chǔ)的是指向重量級(jí)鎖的指針锄贷,此時(shí)等待鎖的線程都會(huì)進(jìn)入阻塞狀態(tài)译蒂。
整體的鎖狀態(tài)升級(jí)流程如下:
綜上,偏向鎖通過對(duì)比Mark Word解決加鎖問題谊却,避免執(zhí)行CAS操作柔昼。而輕量級(jí)鎖是通過用CAS操作和自旋來解決加鎖問題,避免線程阻塞和喚醒而影響性能炎辨。重量級(jí)鎖是將除了擁有鎖的線程以外的線程都阻塞捕透。
4.公平鎖 VS 非公平鎖
公平鎖是指多個(gè)線程按照申請(qǐng)鎖的順序來獲取鎖,線程直接進(jìn)入隊(duì)列中排隊(duì)碴萧,隊(duì)列中的第一個(gè)線程才能獲得鎖乙嘀。公平鎖的優(yōu)點(diǎn)是等待鎖的線程不會(huì)餓死。缺點(diǎn)是整體吞吐效率相對(duì)非公平鎖要低破喻,等待隊(duì)列中除第一個(gè)線程以外的所有線程都會(huì)阻塞虎谢,CPU喚醒阻塞線程的開銷比非公平鎖大。
非公平鎖是多個(gè)線程加鎖時(shí)直接嘗試獲取鎖曹质,獲取不到才會(huì)到等待隊(duì)列的隊(duì)尾等待嘉冒。但如果此時(shí)鎖剛好可用,那么這個(gè)線程可以無(wú)需阻塞直接獲取到鎖咆繁,所以非公平鎖有可能出現(xiàn)后申請(qǐng)鎖的線程先獲取鎖的場(chǎng)景讳推。非公平鎖的優(yōu)點(diǎn)是可以減少喚起線程的開銷,整體的吞吐效率高玩般,因?yàn)榫€程有幾率不阻塞直接獲得鎖银觅,CPU不必喚醒所有線程。缺點(diǎn)是處于等待隊(duì)列中的線程可能會(huì)餓死坏为,或者等很久才會(huì)獲得鎖究驴。
直接用語(yǔ)言描述可能有點(diǎn)抽象,這里作者用從別處看到的一個(gè)例子來講述一下公平鎖和非公平鎖匀伏。
如上圖所示洒忧,假設(shè)有一口水井,有管理員看守够颠,管理員有一把鎖熙侍,只有拿到鎖的人才能夠打水,打完水要把鎖還給管理員。每個(gè)過來打水的人都要管理員的允許并拿到鎖之后才能去打水蛉抓,如果前面有人正在打水庆尘,那么這個(gè)想要打水的人就必須排隊(duì)。管理員會(huì)查看下一個(gè)要去打水的人是不是隊(duì)伍里排最前面的人巷送,如果是的話驶忌,才會(huì)給你鎖讓你去打水;如果你不是排第一的人笑跛,就必須去隊(duì)尾排隊(duì)付魔,這就是公平鎖。
但是對(duì)于非公平鎖飞蹂,管理員對(duì)打水的人沒有要求抒抬。即使等待隊(duì)伍里有排隊(duì)等待的人,但如果在上一個(gè)人剛打完水把鎖還給管理員而且管理員還沒有允許等待隊(duì)伍里下一個(gè)人去打水時(shí)晤柄,剛好來了一個(gè)插隊(duì)的人,這個(gè)插隊(duì)的人是可以直接從管理員那里拿到鎖去打水妖胀,不需要排隊(duì)芥颈,原本排隊(duì)等待的人只能繼續(xù)等待。如下圖所示:
接下來我們通過ReentrantLock的源碼來講解公平鎖和非公平鎖赚抡。
根據(jù)代碼可知爬坑,ReentrantLock里面有一個(gè)內(nèi)部類Sync,Sync繼承AQS(AbstractQueuedSynchronizer)涂臣,添加鎖和釋放鎖的大部分操作實(shí)際上都是在Sync中實(shí)現(xiàn)的盾计。它有公平鎖FairSync和非公平鎖NonfairSync兩個(gè)子類。ReentrantLock默認(rèn)使用非公平鎖赁遗,也可以通過構(gòu)造器來顯示的指定使用公平鎖署辉。
下面我們來看一下公平鎖與非公平鎖的加鎖方法的源碼:
通過上圖中的源代碼對(duì)比,我們可以明顯的看出公平鎖與非公平鎖的lock()方法唯一的區(qū)別就在于公平鎖在獲取同步狀態(tài)時(shí)多了一個(gè)限制條件:hasQueuedPredecessors()岩四。
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
再進(jìn)入hasQueuedPredecessors()哭尝,可以看到該方法主要做一件事情:主要是判斷當(dāng)前線程是否位于同步隊(duì)列中的第一個(gè)。如果是則返回true剖煌,否則返回false材鹦。
綜上,公平鎖就是通過同步隊(duì)列來實(shí)現(xiàn)多個(gè)線程按照申請(qǐng)鎖的順序來獲取鎖耕姊,從而實(shí)現(xiàn)公平的特性桶唐。非公平鎖加鎖時(shí)不考慮排隊(duì)等待問題,直接嘗試獲取鎖茉兰,所以存在后申請(qǐng)卻先獲得鎖的情況尤泽。
5. 可重入鎖 VS 非可重入鎖
可重入鎖又名遞歸鎖,是指在同一個(gè)線程在外層方法獲取鎖的時(shí)候,再進(jìn)入該線程的內(nèi)層方法會(huì)自動(dòng)獲取鎖(前提鎖對(duì)象得是同一個(gè)對(duì)象或者class)安吁,不會(huì)因?yàn)橹耙呀?jīng)獲取過還沒釋放而阻塞醉蚁。Java中ReentrantLock和synchronized都是可重入鎖,可重入鎖的一個(gè)優(yōu)點(diǎn)是可一定程度避免死鎖鬼店。下面用示例代碼來進(jìn)行分析:
public class Widget {
public synchronized void doSomething() {
System.out.println("方法1執(zhí)行...");
doOthers();
}
public synchronized void doOthers() {
System.out.println("方法2執(zhí)行...");
}
}
在上面的代碼中网棍,類中的兩個(gè)方法都是被內(nèi)置鎖synchronized修飾的,doSomething()方法中調(diào)用doOthers()方法妇智。因?yàn)閮?nèi)置鎖是可重入的滥玷,所以同一個(gè)線程在調(diào)用doOthers()時(shí)可以直接獲得當(dāng)前對(duì)象的鎖,進(jìn)入doOthers()進(jìn)行操作巍棱。
如果是一個(gè)不可重入鎖惑畴,那么當(dāng)前線程在調(diào)用doOthers()之前需要將執(zhí)行doSomething()時(shí)獲取當(dāng)前對(duì)象的鎖釋放掉,實(shí)際上該對(duì)象鎖已被當(dāng)前線程所持有航徙,且無(wú)法釋放如贷。所以此時(shí)會(huì)出現(xiàn)死鎖。
而為什么可重入鎖就可以在嵌套調(diào)用時(shí)可以自動(dòng)獲得鎖呢到踏?我們通過圖示和源碼來分別解析一下杠袱。
還是打水的例子,有多個(gè)人在排隊(duì)打水,此時(shí)管理員允許鎖和同一個(gè)人的多個(gè)水桶綁定。這個(gè)人用多個(gè)水桶打水時(shí)撵幽,第一個(gè)水桶和鎖綁定并打完水之后,第二個(gè)水桶也可以直接和鎖綁定并開始打水纹蝴,所有的水桶都打完水之后打水人才會(huì)將鎖還給管理員。這個(gè)人的所有打水流程都能夠成功執(zhí)行踪少,后續(xù)等待的人也能夠打到水塘安。這就是可重入鎖。
但如果是非可重入鎖的話援奢,此時(shí)管理員只允許鎖和同一個(gè)人的一個(gè)水桶綁定耙旦。第一個(gè)水桶和鎖綁定打完水之后并不會(huì)釋放鎖,導(dǎo)致第二個(gè)水桶不能和鎖綁定也無(wú)法打水萝究。當(dāng)前線程出現(xiàn)死鎖免都,整個(gè)等待隊(duì)列中的所有線程都無(wú)法被喚醒。
之前我們說過ReentrantLock和synchronized都是重入鎖帆竹,那么我們通過重入鎖ReentrantLock以及非可重入鎖NonReentrantLock的源碼來對(duì)比分析一下為什么非可重入鎖在重復(fù)調(diào)用同步資源時(shí)會(huì)出現(xiàn)死鎖绕娘。
首先ReentrantLock和NonReentrantLock都繼承父類AQS,其父類AQS中維護(hù)了一個(gè)同步狀態(tài)status來計(jì)數(shù)重入次數(shù)栽连,status初始值為0险领。
當(dāng)線程嘗試獲取鎖時(shí)侨舆,可重入鎖先嘗試獲取并更新status值,如果status == 0表示沒有其他線程在執(zhí)行同步代碼绢陌,則把status置為1挨下,當(dāng)前線程開始執(zhí)行。如果status != 0脐湾,則判斷當(dāng)前線程是否是獲取到這個(gè)鎖的線程臭笆,如果是的話執(zhí)行status+1,且當(dāng)前線程可以再次獲取鎖秤掌。而非可重入鎖是直接去獲取并嘗試更新當(dāng)前status的值愁铺,如果status != 0的話會(huì)導(dǎo)致其獲取鎖失敗,當(dāng)前線程阻塞闻鉴。
釋放鎖時(shí)茵乱,可重入鎖同樣先獲取當(dāng)前status的值,在當(dāng)前線程是持有鎖的線程的前提下孟岛。如果status-1 == 0瓶竭,則表示當(dāng)前線程所有重復(fù)獲取鎖的操作都已經(jīng)執(zhí)行完畢,然后該線程才會(huì)真正釋放鎖渠羞。而非可重入鎖則是在確定當(dāng)前線程是持有鎖的線程之后斤贰,直接將status置為0,將鎖釋放堵未。
6. 獨(dú)享鎖 VS 共享鎖
獨(dú)享鎖和共享鎖同樣是一種概念。我們先介紹一下具體的概念盏触,然后通過ReentrantLock和ReentrantReadWriteLock的源碼來介紹獨(dú)享鎖和共享鎖渗蟹。
獨(dú)享鎖也叫排他鎖,是指該鎖一次只能被一個(gè)線程所持有赞辩。如果線程T對(duì)數(shù)據(jù)A加上排它鎖后雌芽,則其他線程不能再對(duì)A加任何類型的鎖。獲得排它鎖的線程即能讀數(shù)據(jù)又能修改數(shù)據(jù)辨嗽。JDK中的synchronized和JUC中Lock的實(shí)現(xiàn)類就是互斥鎖世落。
共享鎖是指該鎖可被多個(gè)線程所持有。如果線程T對(duì)數(shù)據(jù)A加上共享鎖后糟需,則其他線程只能對(duì)A再加共享鎖屉佳,不能加排它鎖。獲得共享鎖的線程只能讀數(shù)據(jù)洲押,不能修改數(shù)據(jù)武花。
獨(dú)享鎖與共享鎖也是通過AQS來實(shí)現(xiàn)的,通過實(shí)現(xiàn)不同的方法杈帐,來實(shí)現(xiàn)獨(dú)享或者共享体箕。
我們看到ReentrantReadWriteLock有兩把鎖:ReadLock和WriteLock专钉,由詞知意,一個(gè)讀鎖一個(gè)寫鎖累铅,合稱“讀寫鎖”跃须。再進(jìn)一步觀察可以發(fā)現(xiàn)ReadLock和WriteLock是靠?jī)?nèi)部類Sync實(shí)現(xiàn)的鎖。Sync是AQS的一個(gè)子類娃兽,這種結(jié)構(gòu)在CountDownLatch菇民、ReentrantLock、Semaphore里面也都存在换薄。
在ReentrantReadWriteLock里面玉雾,讀鎖和寫鎖的鎖主體都是Sync,但讀鎖和寫鎖的加鎖方式不一樣轻要。讀鎖是共享鎖复旬,寫鎖是獨(dú)享鎖。讀鎖的共享鎖可保證并發(fā)讀非常高效冲泥,而讀寫驹碍、寫讀、寫寫的過程互斥凡恍,因?yàn)樽x鎖和寫鎖是分離的志秃。所以ReentrantReadWriteLock的并發(fā)性相比一般的互斥鎖有了很大提升。
那讀鎖和寫鎖的具體加鎖方式有什么區(qū)別呢嚼酝?在了解源碼之前我們需要回顧一下其他知識(shí)浮还。 在最開始提及AQS的時(shí)候我們也提到了state字段(int類型,32位)闽巩,該字段用來描述有多少線程獲持有鎖钧舌。
在獨(dú)享鎖中這個(gè)值通常是0或者1(如果是重入鎖的話state值就是重入的次數(shù)),在共享鎖中state就是持有鎖的數(shù)量涎跨。但是在ReentrantReadWriteLock中有讀洼冻、寫兩把鎖,所以需要在一個(gè)整型變量state上分別描述讀鎖和寫鎖的數(shù)量(或者也可以叫狀態(tài))隅很。于是將state變量“按位切割”切分成了兩個(gè)部分撞牢,高16位表示讀鎖狀態(tài)(讀鎖個(gè)數(shù)),低16位表示寫鎖狀態(tài)(寫鎖個(gè)數(shù))叔营。如下圖所示:
了解了概念之后我們?cè)賮砜创a屋彪,先看寫鎖的加鎖源碼:
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState(); // 取到當(dāng)前鎖的個(gè)數(shù)
int w = exclusiveCount(c); // 取寫鎖的個(gè)數(shù)w
if (c != 0) { // 如果已經(jīng)有線程持有了鎖(c!=0)
// (Note: if c != 0 and w == 0 then shared count != 0)
if (w == 0 || current != getExclusiveOwnerThread()) // 如果寫線程數(shù)(w)為0(換言之存在讀鎖) 或者持有鎖的線程不是當(dāng)前線程就返回失敗
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT) // 如果寫入鎖的數(shù)量大于最大數(shù)(65535,2的16次方-1)就拋出一個(gè)Error绒尊。
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
setState(c + acquires);
return true;
}
if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) // 如果當(dāng)且寫線程數(shù)為0撼班,并且當(dāng)前線程需要阻塞那么就返回失敗垒酬;或者如果通過CAS增加寫線程數(shù)失敗也返回失敗砰嘁。
return false;
setExclusiveOwnerThread(current); // 如果c=0件炉,w=0或者c>0,w>0(重入)矮湘,則設(shè)置當(dāng)前線程或鎖的擁有者
return true;
}
- 這段代碼首先取到當(dāng)前鎖的個(gè)數(shù)c斟冕,然后再通過c來獲取寫鎖的個(gè)數(shù)w。因?yàn)閷戞i是低16位缅阳,所以取低16位的最大值與當(dāng)前的c做與運(yùn)算( int w = exclusiveCount?; )磕蛇,高16位和0與運(yùn)算后是0,剩下的就是低位運(yùn)算的值十办,同時(shí)也是持有寫鎖的線程數(shù)目秀撇。
- 在取到寫鎖線程的數(shù)目后,首先判斷是否已經(jīng)有線程持有了鎖向族。如果已經(jīng)有線程持有了鎖(c!=0)呵燕,則查看當(dāng)前寫鎖線程的數(shù)目,如果寫線程數(shù)為0(即此時(shí)存在讀鎖)或者持有鎖的線程不是當(dāng)前線程就返回失敿唷(涉及到公平鎖和非公平鎖的實(shí)現(xiàn))再扭。
- 如果寫入鎖的數(shù)量大于最大數(shù)(65535,2的16次方-1)就拋出一個(gè)Error夜矗。
- 如果當(dāng)且寫線程數(shù)為0(那么讀線程也應(yīng)該為0泛范,因?yàn)樯厦嬉呀?jīng)處理c!=0的情況),并且當(dāng)前線程需要阻塞那么就返回失斘伤骸罢荡;如果通過CAS增加寫線程數(shù)失敗也返回失敗。
- 如果c=0,w=0或者c>0,w>0(重入)对扶,則設(shè)置當(dāng)前線程或鎖的擁有者区赵,返回成功!
tryAcquire()除了重入條件(當(dāng)前線程為獲取了寫鎖的線程)之外辩稽,增加了一個(gè)讀鎖是否存在的判斷惧笛。如果存在讀鎖从媚,則寫鎖不能被獲取逞泄,原因在于:必須確保寫鎖的操作對(duì)讀鎖可見,如果允許讀鎖在已被獲取的情況下對(duì)寫鎖的獲取拜效,那么正在運(yùn)行的其他讀線程就無(wú)法感知到當(dāng)前寫線程的操作喷众。
因此,只有等待其他讀線程都釋放了讀鎖紧憾,寫鎖才能被當(dāng)前線程獲取到千,而寫鎖一旦被獲取,則其他讀寫線程的后續(xù)訪問均被阻塞赴穗。寫鎖的釋放與ReentrantLock的釋放過程基本類似憔四,每次釋放均減少寫狀態(tài)膀息,當(dāng)寫狀態(tài)為0時(shí)表示寫鎖已被釋放,然后等待的讀寫線程才能夠繼續(xù)訪問讀寫鎖了赵,同時(shí)前次寫線程的修改對(duì)后續(xù)的讀寫線程可見潜支。
接著是讀鎖的代碼:
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1; // 如果其他線程已經(jīng)獲取了寫鎖,則當(dāng)前線程獲取讀鎖失敗柿汛,進(jìn)入等待狀態(tài)
int r = sharedCount(c);
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current);
}
可以看到在tryAcquireShared(int unused)方法中冗酿,如果其他線程已經(jīng)獲取了寫鎖,則當(dāng)前線程獲取讀鎖失敗络断,進(jìn)入等待狀態(tài)裁替。如果當(dāng)前線程獲取了寫鎖或者寫鎖未被獲取,則當(dāng)前線程(線程安全貌笨,依靠CAS保證)增加讀狀態(tài)弱判,成功獲取讀鎖。讀鎖的每次釋放(線程安全的躁绸,可能有多個(gè)讀線程同時(shí)釋放讀鎖)均減少讀狀態(tài)裕循,減少的值是“1<<16”。所以讀寫鎖才能實(shí)現(xiàn)讀讀的過程共享净刮,而讀寫剥哑、寫讀、寫寫的過程互斥淹父。
此時(shí)株婴,我們?cè)倩仡^看一下互斥鎖ReentrantLock中公平鎖和非公平鎖的加鎖源碼:
我們發(fā)現(xiàn)在ReentrantLock雖然有公平鎖和非公平鎖兩種,但是它們添加的都是獨(dú)享鎖暑认。根據(jù)源碼所示困介,當(dāng)某一個(gè)線程調(diào)用lock方法獲取鎖時(shí),如果同步資源沒有被其他線程鎖住蘸际,那么當(dāng)前線程在使用CAS更新state成功后就會(huì)成功搶占該資源座哩。而如果公共資源被占用且不是被當(dāng)前線程占用,那么就會(huì)加鎖失敗粮彤。所以可以確定ReentrantLock無(wú)論讀操作還是寫操作根穷,添加的鎖都是都是獨(dú)享鎖。
結(jié)語(yǔ)
本文Java中常用的鎖以及常見的鎖的概念進(jìn)行了基本介紹导坟,并從源碼以及實(shí)際應(yīng)用的角度進(jìn)行了對(duì)比分析屿良。限于篇幅以及個(gè)人水平,沒有在本篇文章中對(duì)所有內(nèi)容進(jìn)行深層次的講解惫周。
其實(shí)Java本身已經(jīng)對(duì)鎖本身進(jìn)行了良好的封裝尘惧,降低了研發(fā)同學(xué)在平時(shí)工作中的使用難度。但是研發(fā)同學(xué)也需要熟悉鎖的底層原理递递,不同場(chǎng)景下選擇最適合的鎖喷橙。而且源碼中的思路都是非常好的思路啥么,也是值得大家去學(xué)習(xí)和借鑒的。
參考資料
- 《Java并發(fā)編程藝術(shù)》
- Java中的鎖
- Java CAS 原理剖析
- Java并發(fā)——關(guān)鍵字synchronized解析
- Java synchronized原理總結(jié)
- 聊聊并發(fā)(二)——Java SE1.6中的Synchronized
- 深入理解讀寫鎖—ReadWriteLock源碼分析
- 【JUC】JDK1.8源碼分析之ReentrantReadWriteLock
- Java多線程(十)之ReentrantReadWriteLock深入分析
- Java–讀寫鎖的實(shí)現(xiàn)原理
作者簡(jiǎn)介
家琪贰逾,美團(tuán)點(diǎn)評(píng)后端工程師饥臂。2017 年加入美團(tuán)點(diǎn)評(píng),負(fù)責(zé)美團(tuán)點(diǎn)評(píng)境內(nèi)度假的業(yè)務(wù)開發(fā)似踱。