问懵逼:请站在 JVM 角度谈谈 Java 的锁?
并發(fā)是從JDK 5升級(jí)到JDK 6后一項(xiàng)重要的改進(jìn)項(xiàng),HotSpot虛擬機(jī)開(kāi)發(fā)團(tuán)隊(duì)在這個(gè)版本上花費(fèi)了大量的資源去實(shí)現(xiàn)各種鎖優(yōu)化技術(shù),如適應(yīng)性自旋(Adaptive Spinning)、鎖消除(Lock Elimination)、鎖膨脹(Lock Coarsening)、輕量級(jí)鎖(Lightweight Locking)、偏向鎖(Biased Locking)等,這些技術(shù)都是為了在線程之間更高效地共享數(shù)據(jù)及解決競(jìng)爭(zhēng)問(wèn)題,從而提高程序的執(zhí)行效率 .
存在的問(wèn)題
對(duì)于最開(kāi)始 (JDK1.5之前), Java的同步只能是一個(gè)synchronized修飾, 進(jìn)行同步, 但是這個(gè)由很大的問(wèn)題. 只會(huì)有一個(gè)線程可以entermonitor , 然后計(jì)數(shù)器+1. 稱為重量級(jí)鎖. 其他線程都被掛起, 我們知道對(duì)于大多數(shù)JVM來(lái)說(shuō), 線程是和操作系統(tǒng)的線程是一一綁定的, 也就是我操作的線程掛起需要由內(nèi)核來(lái)完成, 這時(shí)候就需要用戶態(tài)轉(zhuǎn)換到內(nèi)核態(tài) ,然后內(nèi)核執(zhí)行此線程掛起, 當(dāng)要恢復(fù)線程的時(shí)候再通知內(nèi)核, ?此時(shí)會(huì)造成很嚴(yán)重的問(wèn)題 . 我們知道對(duì)于CPU來(lái)說(shuō), 他是靠時(shí)間片來(lái)實(shí)現(xiàn)的多線程并行執(zhí)行, 如果我一個(gè)同步任務(wù)只會(huì)比如count++ , 他執(zhí)行很短, 短到幾ns級(jí)別, 而掛起線程和恢復(fù)線程的實(shí)現(xiàn)遠(yuǎn)遠(yuǎn)大于幾ns?, 可能大幾個(gè)量級(jí) .
因此聰明的人想到一個(gè)事情, 就是我不讓你掛起, 這么短我就自己空轉(zhuǎn)一會(huì), 也很短, (空轉(zhuǎn)的意思其實(shí)就是while(true) 啥也不做,但是不是讓CPU掛起,這個(gè)也稱之為自旋) , 我們知道空轉(zhuǎn)就是一種浪費(fèi)CPU的事情 , 但是這個(gè)浪費(fèi)得有個(gè)度 , 我們上訴的問(wèn)題, 每個(gè)線程可能空轉(zhuǎn)的時(shí)間也就幾ns , 但是對(duì)于長(zhǎng)到幾秒的還能空轉(zhuǎn)嗎, 不行了. 所以這里就是一個(gè)劃分點(diǎn).
還有一個(gè)問(wèn)題就是, 比如某一段時(shí)間內(nèi), 就一個(gè)線程處于運(yùn)作中, 那么此時(shí)還需要加鎖操作嗎 ? 是否需要優(yōu)化 .
因此引出了下文的解決方案.
自旋鎖
自旋鎖是JDK1.4.2的時(shí)候引入的, 默認(rèn)為關(guān)閉狀態(tài), 可以使用-XX:+UseSpinning參數(shù)來(lái)開(kāi)啟 , 但是這個(gè)自旋鎖他不是一直的自旋, 他有個(gè)度, 這個(gè)度可以用-XX:PreBlockSpin?來(lái)控制自旋多少次, 默認(rèn)是10次.
自適應(yīng)自旋
JDK 6中對(duì)自旋鎖的優(yōu)化,引入了自適應(yīng)的自旋。
**自適應(yīng)意味著自旋的時(shí)間不再是固定的了,而是由前一次在同一個(gè)鎖上的自旋時(shí)間及鎖的擁有者的狀態(tài)來(lái)決定的。**如果在同一個(gè)鎖對(duì)象上,自旋等待剛剛成功獲得過(guò)鎖,并且持有鎖的線程正在運(yùn)行中,那么虛擬機(jī)就會(huì)認(rèn)為這次自旋也很有可能再次成功,進(jìn)而允許自旋等待持續(xù)相對(duì)更長(zhǎng)的時(shí)間,比如持續(xù)100次忙循環(huán)。另一方面,如果對(duì)于某個(gè)鎖,自旋很少成功獲得過(guò)鎖,那在以后要獲取這個(gè)鎖時(shí)將有可能直接省略掉自旋過(guò)程,以避免浪費(fèi)處理器資源。有了自適應(yīng)自旋,隨著程序運(yùn)行時(shí)間的增長(zhǎng)及性能監(jiān)控信息的不斷完善,虛擬機(jī)對(duì)程序鎖的狀況預(yù)測(cè)就會(huì)越來(lái)越精準(zhǔn),虛擬機(jī)就會(huì)變得越來(lái)越“聰明”了。
Java 對(duì)象的內(nèi)存布局(重要)
了解輕量級(jí)鎖和偏向鎖 需要了解Java對(duì)象的內(nèi)存布局.
再看下面之前 , 要了了解一個(gè)JAVA對(duì)象的內(nèi)存結(jié)構(gòu) , 也稱之為對(duì)象的內(nèi)存布局
對(duì)象頭 :
1、對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù)( MarkWord )
存儲(chǔ) hashCode、GC 分代年齡、鎖類型標(biāo)記、偏向鎖線程 ID 、CAS 鎖指向線程 LockRecord 的指針等,synconized 鎖的機(jī)制與這個(gè)部分( markwork )密切相關(guān),用 markword 中最低的三位代表鎖的狀態(tài),其中一位是偏向鎖位,另外兩位是普通鎖位。
關(guān)于markword , ?這個(gè)是32位操作系統(tǒng)的實(shí)現(xiàn),
2、對(duì)象類型指針( Class Pointer )
對(duì)象指向它的類元數(shù)據(jù)的指針(這個(gè)指針類似于C語(yǔ)言的指針, 指針大小是根據(jù)操作系統(tǒng)決定的,64位好像是8個(gè)字節(jié)大小, 因?yàn)?4位系統(tǒng)的尋址空間很大), JVM 就是通過(guò)它來(lái)確定是哪個(gè) Class 的實(shí)例。
如果是數(shù)組對(duì)象,還會(huì)有一個(gè)額外的部分用于存儲(chǔ)數(shù)組長(zhǎng)度。因?yàn)樘摂M機(jī)可以通過(guò)普通Java對(duì)象的元數(shù)據(jù)信息確定Java對(duì)象的大小,但是如果數(shù)組的長(zhǎng)度是不確定的,將無(wú)法通過(guò)元數(shù)據(jù)中的信息推斷出數(shù)組的大小。也就是arr.len調(diào)用很方便.
實(shí)例數(shù)據(jù)區(qū)域
此處存儲(chǔ)的是對(duì)象真正有效的信息,比如對(duì)象中所有字段的內(nèi)容?. ,無(wú)論是從父類繼承下來(lái)的,還是在子類中定義的字段都必須記錄起來(lái)。
這部分的存儲(chǔ)順序會(huì)受到虛擬機(jī)分配策略參數(shù)(-XX:FieldsAllocationStyle參數(shù))和字段在Java源碼中定義順序的影響。HotSpot虛擬機(jī)默認(rèn)的分配順序?yàn)?strong>longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers,OOPs)(這里基本可以確定Java的類型也就8種),從以上默認(rèn)的分配策略中可以看到,相同寬度的字段總是被分配到一起存放,在滿足這個(gè)前提條件的情況下,在父類中定義的變量會(huì)出現(xiàn)在子類之前。如果HotSpot虛擬機(jī)的+XX:CompactFields參數(shù)值為true(默認(rèn)就為true),那子類之中較窄的變量也允許插入父類變量的空隙之中,以節(jié)省出一點(diǎn)點(diǎn)空間。
對(duì)齊填充
對(duì)象的第三部分是對(duì)齊填充,這并不是必然存在的,也沒(méi)有特別的含義,它僅僅起著占位符的作用。由于HotSpot虛擬機(jī)的自動(dòng)內(nèi)存管理系統(tǒng)要求對(duì)象起始地址必須是8字節(jié)的整數(shù)倍,換句話說(shuō)就是任何對(duì)象的大小都必須是8字節(jié)的整數(shù)倍。對(duì)象頭部分已經(jīng)被精心設(shè)計(jì)成正好是8字節(jié)的倍數(shù)(1倍或者2倍),因此,如果對(duì)象實(shí)例數(shù)據(jù)部分沒(méi)有對(duì)齊的話,就需要通過(guò)對(duì)齊填充來(lái)補(bǔ)全。
其實(shí)也是為了存儲(chǔ)方便.
如果你還是對(duì)上述不理解的話, 你就看看 <深入理解Java虛擬機(jī)> , 里面有. 接下來(lái)就看看具體內(nèi)容了 .
synchronized 鎖升級(jí)流程
synchronized 鎖并不是直接進(jìn)去就是一個(gè)重量級(jí)鎖, 而是有所思考的, 因?yàn)楹芏喽痰牟僮?并不需要掛起線程. 所以類似于空轉(zhuǎn) , 還有就是單線程加鎖. 何必掛起線程呢, 所以sync也幫助我們解決了這個(gè)問(wèn)題.
偏向鎖
在 JDK1.8 中,其實(shí)默認(rèn)是輕量級(jí)鎖,但如果設(shè)定了-XX:BiasedLockingStartupDelay=0?,那在對(duì)一個(gè) Object 做 syncronized 的時(shí)候,會(huì)立即上一把偏向鎖。當(dāng)處于偏向鎖狀態(tài)時(shí), markwork 會(huì)記錄當(dāng)前線程 ID。
它的意思是這個(gè)鎖會(huì)偏向于第一個(gè)獲得它的線程,如果在接下來(lái)的執(zhí)行過(guò)程中,該鎖一直沒(méi)有被其他的線程獲取,則持有偏向鎖的線程將永遠(yuǎn)不需要再進(jìn)行同步。偏向鎖解決的問(wèn)題是,?有些時(shí)候就一個(gè)線程在運(yùn)行, 難道還有多線程問(wèn)題嗎, 所以并不需要. 當(dāng)出現(xiàn)第二個(gè)線程去競(jìng)爭(zhēng)的情況下才會(huì)出現(xiàn)降級(jí)?.
原理:??當(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é)束。
輕量級(jí)鎖
當(dāng)下一個(gè)線程參與到偏向鎖競(jìng)爭(zhēng)時(shí),會(huì)先判斷 markword 中保存的線程 ID 是否與這個(gè)線程 ID 相等,如果不相等,會(huì)立即撤銷偏向鎖,升級(jí)為輕量級(jí)鎖。每個(gè)線程在自己的線程棧中生成一個(gè) LockRecord ( LR ),然后每個(gè)線程通過(guò) CAS (自旋 )的操作將鎖對(duì)象頭中的 markwork 設(shè)置為指向自己的 LR 的指針,哪個(gè)線程設(shè)置成功,就意味著獲得鎖。?關(guān)于 synchronized 中此時(shí)執(zhí)行的 CAS 操作是通過(guò) native 的調(diào)用 HotSpot 中 bytecodeInterpreter.cpp 文件 C++ 代碼實(shí)現(xiàn)的,有興趣的可以繼續(xù)深挖。
重量級(jí)鎖
如果鎖競(jìng)爭(zhēng)加劇(如線程自旋次數(shù)或者自旋的線程數(shù)超過(guò)某閾值, JDK1.6 之后,由 JVM 自己控制該規(guī)則),就會(huì)升級(jí)為重量級(jí)鎖。此時(shí)就會(huì)向操作系統(tǒng)申請(qǐng)資源,線程掛起,進(jìn)入到操作系統(tǒng)內(nèi)核態(tài)的等待隊(duì)列中,等待操作系統(tǒng)調(diào)度,然后映射回用戶態(tài)。在重量級(jí)鎖中,由于需要做內(nèi)核態(tài)到用戶態(tài)的轉(zhuǎn)換,而這個(gè)過(guò)程中需要消耗較多時(shí)間,也就是"重"的原因之一。
可重入
synchronized 擁有強(qiáng)制原子性的內(nèi)部鎖機(jī)制,是一把可重入鎖。因此,在一個(gè)線程使用 synchronized 方法時(shí)調(diào)用該對(duì)象另一個(gè) synchronized 方法,即一個(gè)線程得到一個(gè)對(duì)象鎖后再次請(qǐng)求該對(duì)象鎖,是永遠(yuǎn)可以拿到鎖的。在 Java 中線程獲得對(duì)象鎖的操作是以線程為單位的,而不是以調(diào)用為單位的。synchronized 鎖的對(duì)象頭的 markwork 中會(huì)記錄該鎖的線程持有者和計(jì)數(shù)器,當(dāng)一個(gè)線程請(qǐng)求成功后, JVM 會(huì)記下持有鎖的線程,并將計(jì)數(shù)器計(jì)為1。此時(shí)其他線程請(qǐng)求該鎖,則必須等待。而該持有鎖的線程如果再次請(qǐng)求這個(gè)鎖,就可以再次拿到這個(gè)鎖,同時(shí)計(jì)數(shù)器會(huì)遞增。當(dāng)線程退出一個(gè) ?synchronized 方法/塊時(shí),計(jì)數(shù)器會(huì)遞減,如果計(jì)數(shù)器為 0 則釋放該鎖鎖。
悲觀鎖(互斥鎖、排他鎖)
synchronized 是一把悲觀鎖(獨(dú)占鎖),當(dāng)前線程如果獲取到鎖,會(huì)導(dǎo)致其它所有需要鎖該的線程等待,一直等待持有鎖的線程釋放鎖才繼續(xù)進(jìn)行鎖的爭(zhēng)搶。
總結(jié)
以上是生活随笔為你收集整理的问懵逼:请站在 JVM 角度谈谈 Java 的锁?的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 顺势而为
- 下一篇: 今日头条技术架构到底有多牛?