多線程互斥多線程引入偏向鎖和輕量級(jí)鎖的原因?
? ? ?synchronized的重量級(jí)別的鎖,就是在線程運(yùn)行到該代碼塊的時(shí)候,讓程序的運(yùn)行級(jí)別從用戶態(tài)切換到內(nèi)核態(tài),把所有的線程掛起,讓cpu通過(guò)操作系統(tǒng)指令,去調(diào)度多線程之間,誰(shuí)執(zhí)行代碼塊,誰(shuí)進(jìn)入阻塞狀態(tài)。
? ? ? ?這樣會(huì)頻繁出現(xiàn)程序運(yùn)行狀態(tài)的切換,線程的掛起和喚醒,這樣就會(huì)大量消耗資源,程序運(yùn)行的效率低下。為了提高效率,jvm的開(kāi)發(fā)人員,引入了偏向鎖,和輕量級(jí)鎖,盡量讓多線程訪問(wèn)公共資源的時(shí)候,不進(jìn)行程序運(yùn)行狀態(tài)的切換,由用戶態(tài)進(jìn)入內(nèi)核態(tài),借助操作系統(tǒng)進(jìn)行互斥。
? ? ? ?Jvm規(guī)范中可以看到synchronized在jvm里實(shí)現(xiàn)原理,jvm基于進(jìn)入和退出Monitor對(duì)象來(lái)實(shí)現(xiàn)方法同步和代碼塊同的。在代碼同步的開(kāi)始位置織入monitorenter,在結(jié)束同步的位置(正常結(jié)束和異常結(jié)束處)織入monitorexit指令實(shí)現(xiàn)。
? ? ? 線程執(zhí)行到monitorenter處,將會(huì)獲取鎖對(duì)象鎖對(duì)應(yīng)的monitor的所有權(quán),即嘗試獲得對(duì)象的鎖。(任意對(duì)象都又一個(gè)monitor與之關(guān)聯(lián),當(dāng)且一個(gè)monitor被持有后,他處于鎖定狀態(tài))
? ? ? ??java的多線程安全是基于lock機(jī)制實(shí)現(xiàn)的,而lock的性能往往不如人意。原因是,monitorenter與monitorexit這兩個(gè)控制多線程同步的bytecode原語(yǔ),是jvm依賴操作系統(tǒng)互斥(mutex - fast mutex = futex(可以減少內(nèi)核態(tài)以及資源的切換))來(lái)實(shí)現(xiàn)的。互斥是一種會(huì)導(dǎo)致線程掛起,并在較短時(shí)間內(nèi)又需要重新調(diào)度回原線程的,較為消耗資源的操作。
? ? ? ?為了優(yōu)化java的Lock機(jī)制,從java6開(kāi)始引入輕量級(jí)鎖的概念。輕量級(jí)鎖本意是為了減少多線程進(jìn)入互斥的幾率,并不是要替代互斥。它利用了cpu原語(yǔ)Compare-And-Swap(cas,匯編指令CMPXCHG/LOCK CMPXCHG),嘗試進(jìn)入互斥前,進(jìn)行補(bǔ)救。
引入多種類型的鎖的機(jī)制
在JDK1.6之前,synchonized同步方式的成本非常高,因?yàn)槭褂昧讼到y(tǒng)調(diào)用引起的內(nèi)核態(tài)與用戶態(tài)切換、線程阻塞造成的線程切換等。但是后面改進(jìn)了,引進(jìn)了鎖的四個(gè)狀態(tài),分別是無(wú)鎖,偏向鎖,輕量級(jí)鎖,重量級(jí)鎖,而且是只能逐級(jí)膨脹的。
首先我們要知道,這幾個(gè)級(jí)別適用的場(chǎng)景。
無(wú)鎖:適用于沒(méi)有任何線程發(fā)生沖突或者爭(zhēng)搶的情況,并且沒(méi)有同步標(biāo)識(shí)
偏向鎖:適用于只有一個(gè)線程進(jìn)入同步區(qū)
輕量級(jí)鎖:適用于兩個(gè)線程交替進(jìn)入同步區(qū)
重量級(jí)鎖:適用于多個(gè)線程同時(shí)競(jìng)爭(zhēng)進(jìn)入同步區(qū)這里我就不贅述有關(guān)它們?nèi)齻€(gè)的具體,直接講解如何膨脹。
偏向鎖,輕量級(jí)鎖,重量級(jí)鎖對(duì)比
鎖優(yōu)點(diǎn)缺點(diǎn)適用場(chǎng)景
? ? ? ?偏向鎖加鎖和解鎖不需要額外的消耗,和執(zhí)行非同步方法相比僅存在納秒級(jí)的差距如果線程間存在鎖競(jìng)爭(zhēng),會(huì)帶來(lái)額外的鎖撤銷的消耗適用于只有一個(gè)線程訪問(wèn)同步塊場(chǎng)景。
? ? ? ?輕量級(jí)鎖競(jìng)爭(zhēng)線程不會(huì)阻塞,提高了程序的響應(yīng)速度如果始終得不到索競(jìng)爭(zhēng)的線程,使用自旋會(huì)消耗CPU追求響應(yīng)速度,同步塊執(zhí)行速度非常快重量級(jí)鎖線程競(jìng)爭(zhēng)不使用自旋,不會(huì)消耗CPU線程阻塞,響應(yīng)時(shí)間緩慢追求吞吐量,同步塊執(zhí)行速度較長(zhǎng)?
對(duì)象頭的存儲(chǔ)內(nèi)容(Markword)
長(zhǎng)度內(nèi)容說(shuō)明
32/64bit Mark Word存儲(chǔ)對(duì)象的hashcode或鎖信息(25bit的hashcode編碼)
32/64bit 類對(duì)象的地址存儲(chǔ)到對(duì)象類型數(shù)據(jù)的指針(klasspointer指針)
32/64bit Array length數(shù)組的長(zhǎng)度(如果當(dāng)前對(duì)象是數(shù)組)
Mark Word存儲(chǔ)內(nèi)容(monitor)的狀態(tài)變化
? ? 25bit(hashcode)、4bit (年代數(shù)值)、1bit(是否是偏向鎖)、2bit(鎖標(biāo)示位)
????輕量級(jí)鎖指向棧中鎖記錄的指針?00
????重量級(jí)鎖指向互斥量(重量級(jí)鎖)的指針?10
鎖的狀態(tài)
? 鎖一共有四種狀態(tài)(由低到高的次序):無(wú)鎖狀態(tài),偏向鎖狀態(tài),輕量級(jí)鎖狀態(tài),重量級(jí)鎖狀態(tài)
鎖的等級(jí)只可以升級(jí),不可以降級(jí)。這種鎖升級(jí)卻不能降級(jí)的策略,目的是為了提高獲得鎖和釋放鎖的效率。
偏向鎖
? ? ? ?a線程獲得鎖,會(huì)在a線程的的棧幀里創(chuàng)建lock record(鎖記錄變量),則在鎖對(duì)象的對(duì)象頭里和lock record里存儲(chǔ)a線程的線程id.以后該線程的進(jìn)入,就不需要cas操作,只需要判斷是否是當(dāng)前線程。??
? ? ? a線程獲取鎖,不會(huì)釋放鎖。直到b線程也要競(jìng)爭(zhēng)該鎖時(shí),a線程才會(huì)釋放鎖。
? ? ? ?偏向鎖的釋放,需要等待全局安全點(diǎn)(在這個(gè)時(shí)間點(diǎn)上沒(méi)有正在執(zhí)行的字節(jié)碼),它會(huì)首先暫停擁有偏向鎖的線程,然后檢查持有偏向鎖的線程是否還活著,如果線程不處于活動(dòng)狀態(tài),則將對(duì)象頭設(shè)置成無(wú)鎖狀態(tài)。棧幀中的鎖記錄和對(duì)象頭的Mark Word要么重新偏向其他線程,要么恢復(fù)到無(wú)鎖,或者標(biāo)記對(duì)象不適合作為偏向鎖。最后喚醒暫停的線程。
? ? ? ?如果線程仍然活著,擁有偏向鎖的棧會(huì)被執(zhí)行,遍歷偏向?qū)ο蟮乃涗洝?/b>?
關(guān)閉偏向鎖,通過(guò)jvm的參數(shù)-XX:UseBiasedLocking=false,則默認(rèn)會(huì)進(jìn)入輕量級(jí)鎖。
輕量級(jí)鎖
? ? ? a線程獲得鎖,會(huì)在a線程的棧幀里創(chuàng)建lock record(鎖記錄變量),讓lock record的指針指向鎖對(duì)象的對(duì)象頭中的mark word.再讓mark word 指向lock record.這就是獲取了鎖。
? ? ? 輕量級(jí)鎖,b線程在鎖競(jìng)爭(zhēng)時(shí),發(fā)現(xiàn)鎖已經(jīng)被a線程占用,則b線程不進(jìn)入內(nèi)核態(tài),讓b線程自旋,執(zhí)行空循環(huán),等待a線程釋放鎖。如果,完成CAS自旋策略還是發(fā)現(xiàn)a線程沒(méi)有釋放鎖,或者讓c線程占用了。則b線程試圖將輕量級(jí)鎖升級(jí)為重量級(jí)鎖。
重量級(jí)鎖
? ? ?重量級(jí)鎖,就是讓爭(zhēng)搶鎖的線程從用戶態(tài)轉(zhuǎn)換成內(nèi)核態(tài)。讓cpu借助操作系統(tǒng)進(jìn)行線程協(xié)調(diào)。
1.當(dāng)一個(gè)線程(假設(shè)叫A線程)想要獲得鎖時(shí),首先檢查對(duì)象頭中的鎖標(biāo)志,如果是偏向鎖,則跳轉(zhuǎn)到2,如果是無(wú)鎖狀態(tài),則跳轉(zhuǎn)到3。
2.檢查對(duì)象頭中的偏向線程id是否指向A線程,是,則直接執(zhí)行同步代碼塊,不是則3。
3.使用cas操作將替換對(duì)象頭中的偏向線程id,成功,則直接執(zhí)行同步代碼塊。失敗則說(shuō)明其他的線程(假設(shè)叫B線程)已經(jīng)擁有偏向鎖了,那么進(jìn)行偏向鎖的撤銷(因?yàn)檫@里有競(jìng)爭(zhēng)了),此時(shí)執(zhí)行4。
4.B線程運(yùn)行到全局安全點(diǎn)后,暫停該線程,檢查它的狀態(tài),如果處于不活動(dòng)或者已經(jīng)退出同步代碼塊則原持有偏向鎖的線程釋放鎖,然后A再次執(zhí)行3。如果仍處于活動(dòng)狀態(tài),則需要升級(jí)為輕量級(jí)鎖,此時(shí)執(zhí)行5.
5.在B線程的棧中分配鎖記錄,拷貝對(duì)象頭中的MarkWord到鎖記錄中,然后將MarkWord改為指向B線程,同時(shí)將對(duì)象頭中的鎖標(biāo)志信息改為輕量級(jí)鎖的00,然后喚醒B線程,也就是從安全點(diǎn)處繼續(xù)執(zhí)行。
6.由于鎖升級(jí)為輕量級(jí)鎖,A線程也進(jìn)行相同的操作,即,在A線程的棧中分配鎖記錄,拷貝對(duì)象頭中的Mark Word到鎖記錄中,然后使用cas操作替換MarkWord,因?yàn)榇藭r(shí)B線程擁有鎖,因此,A線程自旋。如果自旋一定次數(shù)內(nèi)成功獲得鎖,那么A線程獲得輕量級(jí)鎖,執(zhí)行同步代碼塊。若自旋后仍未獲得鎖,A升級(jí)為重量級(jí)鎖,將對(duì)象頭中的鎖標(biāo)志信息改為重量級(jí)的10,同時(shí)阻塞,此時(shí)請(qǐng)看7.
7.B線程在釋放鎖的時(shí)候,使用cas將MarkWord中的信息替換,成功,則表示無(wú)競(jìng)爭(zhēng)(這個(gè)時(shí)候還是輕量級(jí)鎖,A線程可能正在自旋中)直接釋放。失?。ㄒ?yàn)檫@個(gè)時(shí)候鎖已經(jīng)膨脹),那么釋放之后喚醒被掛起的線程(在這個(gè)例子中,也就是A)。
以上就是我理解的鎖膨脹過(guò)程。有錯(cuò)誤的地方,歡迎指正。
自旋鎖,也就是如果持有鎖的線程能很短時(shí)間內(nèi)釋放鎖,那么競(jìng)爭(zhēng)鎖的線程就不需要進(jìn)入阻塞掛起狀態(tài),而是等一會(huì)(自旋),這樣能避免用戶線程和內(nèi)核線程的切換消耗,但是如果超過(guò)一定時(shí)間仍未得到,還是會(huì)進(jìn)入阻塞。通過(guò)自旋鎖,可以減少線程阻塞造成的線程切換(包括掛起線程和恢復(fù)線程)。
1.6之后引入了自適應(yīng)自旋鎖,是由前一次在同一個(gè)鎖上的自旋時(shí)間及鎖的擁有者的狀態(tài)來(lái)決定。自適應(yīng)自旋解決的是“鎖競(jìng)爭(zhēng)時(shí)間不確定”的問(wèn)題。JVM很難感知到確切的鎖競(jìng)爭(zhēng)時(shí)間,而交給用戶分析就違反了JVM的設(shè)計(jì)初衷。自適應(yīng)自旋假定不同線程持有同一個(gè)鎖對(duì)象的時(shí)間基本相當(dāng),競(jìng)爭(zhēng)程度趨于穩(wěn)定,因此,可以根據(jù)上一次自旋的時(shí)間與結(jié)果調(diào)整下一次自旋的時(shí)間。
鎖特點(diǎn)分析
偏向鎖的加鎖和解鎖不需要額外的消耗,和執(zhí)行非同步方法比僅存在納秒級(jí)的差距。
如果線程間存在鎖競(jìng)爭(zhēng),會(huì)帶來(lái)額外的鎖撤銷的消耗。
如果只有一個(gè)線程訪問(wèn)同步塊場(chǎng)景:輕量級(jí)鎖競(jìng)爭(zhēng)的線程不會(huì)阻塞,提高了程序的響應(yīng)速度。
如果始終得不到鎖競(jìng)爭(zhēng)的線程使用自旋會(huì)消耗CPU追求響應(yīng)時(shí)間,鎖占用時(shí)間很短重量級(jí)鎖線程競(jìng)爭(zhēng)不使用自旋,不會(huì)消耗CPU線程阻塞,響應(yīng)時(shí)間緩慢追求吞吐量,鎖占用時(shí)間較長(zhǎng)?。