要想構(gòu)建高并發(fā)的應(yīng)用修然,那么多線程就是繞不開的技術(shù)徊哑,而鎖又是多線程中的重點(diǎn)袜刷。如何利用好鎖,將很大程度決定程序的健壯性跟并發(fā)度莺丑。這次我們一起聊下鎖相關(guān)的一些知識(shí)著蟹。
Mark Word
在HotSpot虛擬機(jī)中,對(duì)象在內(nèi)存中存儲(chǔ)的布局可以分為3塊區(qū)域:對(duì)象頭(Header)梢莽、實(shí)例數(shù)據(jù)(Instance Data)和對(duì)齊填充(Padding)萧豆。
而對(duì)象頭又包括以下三部分信息,其中Mark Word是synchronized鎖的實(shí)現(xiàn)基礎(chǔ)昏名。
- Mark Word:用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù)涮雷,如哈希碼(HashCode)、GC分代年齡轻局、鎖狀態(tài)標(biāo)志洪鸭、線程持有的鎖、偏向線程ID仑扑、偏向時(shí)間戳等览爵,這部分?jǐn)?shù)據(jù)的長(zhǎng)度在32位和64位的虛擬機(jī)(未開啟壓縮指針)中分別為32bit和64bit
- Klass Pointer:類型指針,即對(duì)象指向它的類元數(shù)據(jù)的指針夫壁,虛擬機(jī)通過這個(gè)指針來確定這個(gè)對(duì)象是哪個(gè)類的實(shí)例
- 數(shù)組長(zhǎng)度:只有數(shù)組對(duì)象有拾枣,用于記錄數(shù)組的長(zhǎng)度
Mark Word在32位跟64位虛擬機(jī)中存儲(chǔ)結(jié)構(gòu)是不同的,但是核心結(jié)構(gòu)一致盒让,我們就以32位為例進(jìn)行介紹梅肤。
- 鎖標(biāo)志位:區(qū)分鎖狀態(tài),11時(shí)表示對(duì)象待GC回收狀態(tài)
- 是否偏向鎖:由于無鎖和偏向鎖的鎖標(biāo)識(shí)都是01邑茄,這里引入一位的偏向鎖標(biāo)識(shí)位
- 分代年齡:表示對(duì)象被GC的次數(shù)姨蝴,當(dāng)該次數(shù)到達(dá)閾值的時(shí)候,對(duì)象就會(huì)轉(zhuǎn)移到老年代
- 對(duì)象的hashcode:當(dāng)對(duì)象加鎖后肺缕,計(jì)算的結(jié)果32位不夠表示左医,在偏向鎖授帕、輕量鎖、重量鎖浮梢,hashcode會(huì)被轉(zhuǎn)移到monitor中
- 偏向鎖的線程ID:偏向模式時(shí)候跛十,當(dāng)某個(gè)線程持有對(duì)象的時(shí)候,對(duì)象這里就會(huì)被置為該線程的ID秕硝。 再次進(jìn)入時(shí)候芥映,就無需再進(jìn)行嘗試獲取鎖的動(dòng)作
- Epoch:偏向鎖在CAS鎖操作過程中,偏向性標(biāo)識(shí)远豺,表示對(duì)象更偏向哪個(gè)鎖
- 指向棧中鎖記錄的指針:當(dāng)鎖獲取是無競(jìng)爭(zhēng)的時(shí)奈偏,JVM使用原子操作而不是OS互斥。這種技術(shù)稱為輕量級(jí)鎖定躯护。在輕量級(jí)鎖定的情況下惊来,JVM通過CAS操作在對(duì)象的Mark Word中設(shè)置指向棧中鎖記錄的指針
- 指向互斥鎖(重量級(jí)鎖)的指針:如果兩個(gè)不同的線程同時(shí)在同一個(gè)對(duì)象上競(jìng)爭(zhēng),則必須將輕量級(jí)鎖定升級(jí)到Monitor以管理等待的線程棺滞。在重量級(jí)鎖定的情況下裁蚁,JVM在對(duì)象的Mark Word中設(shè)置指向monitor的指針
鎖的實(shí)現(xiàn)
synchronized
synchronized的實(shí)現(xiàn)
public void addValue(int value) {
synchronized (lock) {
a += value;
}
}
上面這段代碼編譯后可以得到如下的字節(jié)碼數(shù)據(jù):
從字節(jié)碼中可知同步語句塊的實(shí)現(xiàn)使用的是monitorenter和monitorexi指令,其中monitorenter指令指向同步代碼塊的開始位置检眯,monitorexit指令則指明同步代碼塊的結(jié)束位置厘擂。
值得注意的是:一條指令monitorenter可以對(duì)應(yīng)到多條monitorexit 指令。這是因?yàn)镴ava虛擬機(jī)需要確保所獲得的鎖在正常執(zhí)行路徑锰瘸,以及異常執(zhí)行路徑上都能夠被解鎖刽严。也就是說:編譯器將會(huì)確保無論方法通過何種方式完成,方法中調(diào)用過的每條 monitorenter 指令都有執(zhí)行其對(duì)應(yīng)monitorexit指令避凝。
monitorenter舞萄,monitorexi指令其實(shí)都是對(duì)monitor對(duì)象的操作,monitor對(duì)象有個(gè)專門的名字:對(duì)象監(jiān)視器(Object Monitor)管削。每個(gè)Java對(duì)象都存在一個(gè)monitor與之關(guān)聯(lián)倒脓,在HotSpot中monitor是由ObjectMonitor實(shí)現(xiàn)(C++源碼)。下面是ObjectMonitor的數(shù)據(jù)結(jié)構(gòu)含思。
ObjectMonitor() {
_header = NULL;
_count = 0; // 記錄個(gè)數(shù)
_waiters = 0,
_recursions = 0;
_object = NULL;// 存儲(chǔ)Monitor對(duì)象
_owner = NULL;// 持有此鎖的線程
_WaitSet = NULL; // 處于wait狀態(tài)的線程崎弃,會(huì)被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; // 處于等待鎖block狀態(tài)的線程,會(huì)被加入到該列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
- 線程執(zhí)行monitorenter時(shí)候含潘,會(huì)獲取此monitor的所有權(quán)饲做,計(jì)數(shù)器0->1,owner指向此線程
- 如果已經(jīng)是monitor的owner遏弱,再次進(jìn)入時(shí)候計(jì)數(shù)器加1
- 當(dāng)執(zhí)行monitorexit的時(shí)候盆均,計(jì)數(shù)器將減1,直到為0的時(shí)候漱逸,代表鎖已經(jīng)釋放泪姨,其他線程就可以來競(jìng)爭(zhēng)此鎖
synchronized的優(yōu)化
在Java6之前synchronized鎖進(jìn)行狀態(tài)切換都是依賴內(nèi)核態(tài)指令進(jìn)行游沿,而內(nèi)核態(tài)進(jìn)行線程的阻塞、喚醒成本是比較昂貴的肮砾。而很多時(shí)候其實(shí)沒有激烈的鎖競(jìng)爭(zhēng)诀黍,或者鎖的持有只有很短的時(shí)間,那么讓請(qǐng)求鎖的線程“稍等一會(huì)”相比掛起唇敞、喚醒線程成本將更低蔗草。Java6之后于是引入了偏向鎖跟輕量級(jí)鎖來優(yōu)化synchronized的性能,synchronized加鎖的過程于是變成了無鎖-->偏向鎖-->輕量級(jí)鎖-->重量級(jí)鎖疆柔。
偏向鎖
偏向鎖目的是為了消除在無競(jìng)爭(zhēng)情況(在大多數(shù)情況下,鎖不僅不存在多線程競(jìng)爭(zhēng)镶柱,而且總是由同一線程多次獲得)下的獲取鎖消耗旷档。當(dāng)鎖對(duì)象第一次被線程獲取的時(shí)候,虛擬機(jī)將會(huì)把對(duì)象頭中的鎖標(biāo)志位設(shè)置為“01”歇拆、把偏向模式設(shè)置為“1”鞋屈,表示進(jìn)入偏向模式。同時(shí)使用CAS操作把獲取到這個(gè)鎖的線程的ID記錄在對(duì)象的Mark Word之中故觅。如果CAS操作成功厂庇,持有偏向鎖的線程以后每次進(jìn)入這個(gè)鎖相關(guān)的同步塊時(shí),虛擬機(jī)都可以不再進(jìn)行任何同步操作(例如加鎖输吏、解鎖及對(duì)Mark Word的更新操作等)权旷。
一旦出現(xiàn)另外一個(gè)線程去嘗試獲取這個(gè)鎖的情況,偏向模式就馬上宣告結(jié)束贯溅。
如果程序中大多數(shù)的鎖都總是被多個(gè)不同的線程訪問拄氯,那偏向模式就是多余的。有時(shí)候使用參數(shù)-XX:- UseBiasedLocking來禁止偏向鎖優(yōu)化反而可以提升性能它浅。
在JDK15中默認(rèn)禁用了偏向鎖译柏,可以使用XX:+UseBiasedLocking開啟。
輕量級(jí)鎖
如果偏向鎖宣告結(jié)束姐霍,將升級(jí)為輕量級(jí)鎖鄙麦。
執(zhí)行輕量級(jí)鎖的時(shí)候,虛擬機(jī)首先將在當(dāng)前線程的棧幀中建立一個(gè)名為鎖記錄(Lock Record)的空間镊折。然后胯府,虛擬機(jī)將使用CAS操作嘗試把對(duì)象的Mark Word更新為指向Lock Record的指針。如果這個(gè)更新動(dòng)作成功了腌乡,即代表該線程擁有了這個(gè)對(duì)象的鎖盟劫,并且對(duì)象Mark Word的鎖標(biāo)志位將轉(zhuǎn)變?yōu)椤?00”。
如果這個(gè)更新操作失敗了与纽,那就意味著至少存在一條線程與當(dāng)前線程競(jìng)爭(zhēng)獲取該對(duì)象的鎖侣签。首先會(huì)檢查對(duì)象的Mark Word是否指向當(dāng)前線程的棧幀塘装,如果是,說明當(dāng)前線程已經(jīng)擁有了這個(gè)對(duì)象的鎖影所,那直接進(jìn)入同步塊繼續(xù)執(zhí)行蹦肴,否則就說明這個(gè)鎖對(duì)象已經(jīng)被其他線程搶占了。所以出現(xiàn)兩條以上的線程爭(zhēng)用同一個(gè)鎖的情況猴娩,那輕量級(jí)鎖就不再有效阴幌,必須要膨脹為重量級(jí)鎖。
自旋鎖
輕量級(jí)鎖失敗后卷中,虛擬機(jī)為了避免線程真實(shí)地在操作系統(tǒng)層面掛起矛双,還會(huì)進(jìn)行一項(xiàng)稱為自旋鎖的優(yōu)化手段。
自旋本身雖然避免了線程切換的開銷蟆豫,但它是要占用處理器時(shí)間的议忽,所以如果鎖被占用的時(shí)間很短,自旋等待的效果就會(huì)非常好十减,否則那么自旋只會(huì)帶來性能的浪費(fèi)栈幸。因此自旋等待的時(shí)間必須有一定的限度,自旋次數(shù)的默認(rèn)值是十次帮辟,用戶也可以使用參數(shù)-XX:PreBlockSpin來自行更改速址。
重量級(jí)鎖
重量級(jí)鎖就是我們大家印象中的鎖,需要從用戶態(tài)切換到內(nèi)核態(tài)由驹,依賴操作系統(tǒng)的內(nèi)核態(tài)來完成線程之間的切換芍锚,這個(gè)成本非常高。
總的來說偏向鎖是認(rèn)為只有一個(gè)線程在使用鎖荔棉,輕量級(jí)鎖是認(rèn)為獲取鎖的時(shí)候不存在競(jìng)爭(zhēng)闹炉,自旋鎖是認(rèn)為鎖住的代碼將很快執(zhí)行完成。如果不符合假設(shè)將向上膨脹润樱。
Lock
自JDK5起Java類庫中新提供了java.util.concurrent包渣触,其中的java.util.concurrent.locks.Lock接口便成了Java的另一種全新的鎖手段∫既簦基于Lock接口嗅钻,用戶能夠以非塊結(jié)構(gòu)來實(shí)現(xiàn)互斥同步,從而擺脫了語言特性的束縛店展,改為在類庫層面去實(shí)現(xiàn)同步养篓。
java.util.concurrent中的鎖都是基于AQS(AbstractQueuedSynchronizer)實(shí)現(xiàn),AQS核心思想是赂蕴,如果被請(qǐng)求的共享資源空閑柳弄,那么就將當(dāng)前請(qǐng)求資源的線程設(shè)置為有效的工作線程,將共享資源設(shè)置為鎖定狀態(tài);如果共享資源被占用碧注,就需要一定的阻塞等待喚醒機(jī)制來保證鎖分配嚣伐。這個(gè)機(jī)制主要用的是CLH隊(duì)列的變體實(shí)現(xiàn)的,將暫時(shí)獲取不到鎖的線程加入到隊(duì)列中萍丐。
CLH:Craig轩端、Landin and Hagersten隊(duì)列,是單向鏈表逝变,AQS中的隊(duì)列是CLH變體的虛擬雙向隊(duì)列(FIFO)基茵,AQS是通過將每條請(qǐng)求共享資源的線程封裝成一個(gè)節(jié)點(diǎn)來實(shí)現(xiàn)鎖的分配。
主要原理圖如下:
AQS使用一個(gè)Volatile的int類型的成員變量來表示同步狀態(tài)壳影,通過內(nèi)置的FIFO隊(duì)列來完成資源獲取的排隊(duì)工作,通過CAS完成對(duì)State值的修改态贤。
ReentrantLock
ReentrantLock(重入鎖)是Lock接口最常見的一種實(shí)現(xiàn)舱呻,顧名思義,它與synchronized一樣是可
重入的悠汽。在基本用法上,ReentrantLock也與synchronized很相似芥驳,只是代碼寫法上稍有區(qū)別而已柿冲。不過ReentrantLock與synchronized相比增加了一些高級(jí)功能,主要有以下三項(xiàng):等待可中斷兆旬、可實(shí)現(xiàn)公平鎖及鎖可以綁定多個(gè)條件假抄。
ReentrantReadWriteLock
ReentrantReadWriteLock是Lock的另一種實(shí)現(xiàn)方式。ReentrantLock是一個(gè)排他鎖丽猬,同一時(shí)間只允許一個(gè)線程訪問宿饱,而ReentrantReadWriteLock允許多個(gè)讀線程同時(shí)訪問,但不允許寫線程和讀線程脚祟、寫線程和寫線程同時(shí)訪問谬以。相對(duì)于排他鎖,提高了并發(fā)性由桌。在實(shí)際應(yīng)用中为黎,很多情況下對(duì)共享數(shù)據(jù)(如緩存)的訪問都是讀操作遠(yuǎn)多于寫操作,這時(shí)ReentrantReadWriteLock能夠提供比排他鎖更好的并發(fā)性和吞吐量行您。
讀寫鎖對(duì)于同步狀態(tài)的實(shí)現(xiàn)是在一個(gè)整形變量上通過“按位切割使用”:將變量切割成兩部分铭乾,高16位表示讀,低16位表示寫娃循。
假設(shè)當(dāng)前同步狀態(tài)值為S炕檩,get和set的操作如下:
- 獲取寫狀態(tài):S&0x0000FFFF:將高16位全部抹去
- 獲取讀狀態(tài):S>>>16:無符號(hào)補(bǔ)0,右移16位
- 寫狀態(tài)加1: S+1
- 讀狀態(tài)加1: S+(1<<16)即S + 0x00010000
看起來Lock實(shí)現(xiàn)的鎖性能不差于synchronized捌斧,功能又豐富笛质,那么是不是可以棄用synchronized了呢泉沾?答案是否定的,從長(zhǎng)遠(yuǎn)來看经瓷,Java虛擬機(jī)更容易針對(duì)synchronized來進(jìn)行優(yōu)化爆哑,因?yàn)镴ava虛擬機(jī)可以在線程和對(duì)象的元數(shù)據(jù)中記錄synchronized中鎖的相關(guān)信息,也可以在編譯器對(duì)其進(jìn)行鎖消除等優(yōu)化舆吮。而Lock的話揭朝,Java虛擬機(jī)是很難對(duì)其進(jìn)行監(jiān)控、分析色冀,也就難以對(duì)其進(jìn)行優(yōu)化潭袱。所以Java建議在能實(shí)現(xiàn)同樣功能的時(shí)候優(yōu)先選擇使用synchronized。
其他
Mark Word中對(duì)象hashCode的說明
在Java語言里面一個(gè)對(duì)象如果計(jì)算過哈希碼锋恬,就應(yīng)該一直保持該值不變屯换,它通過在對(duì)象頭中存儲(chǔ)計(jì)算結(jié)果來保證第一次計(jì)算之后,再次調(diào)用該方法取到的哈希碼值永遠(yuǎn)不會(huì)再發(fā)生改變与学。因此彤悔,當(dāng)一個(gè)對(duì)象已經(jīng)計(jì)算過一致性哈希碼后,它就再也無法進(jìn)入偏向鎖狀態(tài)了索守。在重量級(jí)鎖的實(shí)現(xiàn)中晕窑,對(duì)象頭指向了重量級(jí)鎖的位置,代表重量級(jí)鎖的Object Monitor類里有字段記錄了非加鎖狀態(tài)(標(biāo)志位為“01”)下的Mark Word卵佛。
自適應(yīng)自旋
無論是自旋的默認(rèn)值還是用戶指定的自旋次數(shù)杨赤,將對(duì)所有的鎖都將生效。在JDK6中引入了自適應(yīng)的自旋截汪。自適應(yīng)意味著自旋的時(shí)間不再固疾牲,而是由前一次在同一個(gè)鎖上的自旋時(shí)間及鎖的擁有者的狀態(tài)來決定的。如果在同一個(gè)鎖對(duì)象上衙解,自旋等待剛剛成功獲得過鎖阳柔,并且持有鎖的線程正在運(yùn)行中,那么虛擬機(jī)就會(huì)認(rèn)為這次自旋也很有可能再次成功丢郊,進(jìn)而允許自旋等待持續(xù)相對(duì)更長(zhǎng)的時(shí)間盔沫。如果自旋很少成功獲得過鎖,那在以后要獲取這個(gè)鎖時(shí)將有可能直接省略掉自旋過程枫匾,以避免浪費(fèi)處理器資源架诞。有了自適應(yīng)自旋,隨著程序運(yùn)行時(shí)間的增長(zhǎng)及性能監(jiān)控信息的不斷完善干茉,虛擬機(jī)對(duì)程序鎖的狀況預(yù)測(cè)就會(huì)越來越精準(zhǔn)谴忧。
鎖的一些名詞
樂觀鎖/悲觀鎖
樂觀鎖認(rèn)為使用共享數(shù)據(jù)的時(shí)候不會(huì)有別的線程來修改,所以只有在更新數(shù)據(jù)的時(shí)候才判定是否有別的線程將數(shù)據(jù)修改了,如果沒有則正常更新沾谓,如果有則按需處理(重新計(jì)算或者報(bào)錯(cuò))委造,最經(jīng)常使用的是CAS。典型案例如Java中AtomicInteger自增操作均驶。
悲觀鎖則認(rèn)為在使用共享數(shù)據(jù)的時(shí)候會(huì)有別的線程來修改昏兆,所以要先加鎖,Java中的synchronized妇穴、Lock都屬于悲觀鎖爬虱。
獨(dú)享鎖/共享鎖
獨(dú)享鎖也叫互斥鎖、排它鎖腾它,是指同一時(shí)間一個(gè)鎖只能被一個(gè)線程持有跑筝,如synchronized、ReentrantLock都屬于獨(dú)享鎖瞒滴。
共享鎖是指同一時(shí)間一個(gè)鎖可以被多個(gè)線程同時(shí)持有曲梗,如ReentrantReadWriteLock中的ReadLock。
公平鎖/非公平鎖
公平鎖是指多個(gè)線程按照申請(qǐng)鎖的順序來獲取鎖妓忍,線程會(huì)先進(jìn)入到隊(duì)列中排隊(duì)虏两,只有隊(duì)列中的第一個(gè)線程才能獲取到鎖。
非公平鎖是多個(gè)線程加鎖是直接嘗試獲取鎖世剖,獲取不到的時(shí)候進(jìn)入隊(duì)列等待或者掛起碘举。
ReentrantLock中的公平鎖與非公平鎖加鎖的時(shí)候區(qū)別如下圖所示,非公平鎖會(huì)先嘗試獲取鎖搁廓,而公平鎖直接進(jìn)入隊(duì)列(當(dāng)然acquire方法的實(shí)現(xiàn)是不同的)。一般來說非公平鎖的吞吐會(huì)比公平鎖的高耕皮。
深入理解Java虛擬機(jī)--周志明
大徹大悟synchronized原理境蜕,鎖的升級(jí)
從ReentrantLock的實(shí)現(xiàn)看AQS的原理及應(yīng)用