看完你就會知道,線程如果鎖住了某個資源,致使其他線程無法訪問的這種鎖被稱為悲觀鎖,相反,線程不鎖住資源的鎖被稱為樂觀鎖,而自旋鎖是基于 CAS 機(jī)制實現(xiàn)的,CAS又是樂觀鎖的一種實現(xiàn),那么對于鎖來說,多個線程同步訪問某個資源的流程細(xì)節(jié)是否一樣呢?換句話說,在多線程同步訪問某個資源時,鎖的狀態(tài)會如何變化呢?本篇文章來探討一下。
站在用戶的角度思考問題,與客戶深入溝通,找到三山網(wǎng)站設(shè)計與三山網(wǎng)站推廣的解決方案,憑借多年的經(jīng)驗,讓設(shè)計與互聯(lián)網(wǎng)技術(shù)結(jié)合,創(chuàng)造個性化、用戶體驗好的作品,建站類型包括:做網(wǎng)站、成都網(wǎng)站制作、企業(yè)官網(wǎng)、英文網(wǎng)站、手機(jī)端網(wǎng)站、網(wǎng)站推廣、申請域名、網(wǎng)絡(luò)空間、企業(yè)郵箱。業(yè)務(wù)覆蓋三山地區(qū)。鎖狀態(tài)的分類
Java 語言專門針對 synchronized 關(guān)鍵字設(shè)置了四種狀態(tài),它們分別是:無鎖、偏向鎖、輕量級鎖和重量級鎖,但是在了解這些鎖之前還需要先了解一下 Java 對象頭和 Monitor。
Java 對象頭
我們知道 synchronized 是悲觀鎖,在操作同步之前需要給資源加鎖,這把鎖就是對象頭里面的,而Java 對象頭又是什么呢?我們以 Hotspot 虛擬機(jī)為例,Hopspot 對象頭主要包括兩部分?jǐn)?shù)據(jù):Mark Word(標(biāo)記字段) 和 Klass Pointer(類型指針)。
Mark Word:默認(rèn)存儲對象的HashCode,分代年齡和鎖標(biāo)志位信息。這些信息都是與對象自身定義無關(guān)的數(shù)據(jù),所以Mark Word被設(shè)計成一個非固定的數(shù)據(jù)結(jié)構(gòu)以便在極小的空間內(nèi)存存儲盡量多的數(shù)據(jù)。它會根據(jù)對象的狀態(tài)復(fù)用自己的存儲空間,也就是說在運(yùn)行期間Mark Word里存儲的數(shù)據(jù)會隨著鎖標(biāo)志位的變化而變化。
Klass Point:對象指向它的類元數(shù)據(jù)的指針,虛擬機(jī)通過這個指針來確定這個對象是哪個類的實例。
在32位虛擬機(jī)和64位虛擬機(jī)的 Mark Word 所占用的字節(jié)大小不一樣,32位虛擬機(jī)的 Mark Word 和 Klass Pointer 分別占用 32bits 的字節(jié),而 64位虛擬機(jī)的 Mark Word 和 Klass Pointer 占用了64bits 的字節(jié),下面我們以 32位虛擬機(jī)為例,來看一下其 Mark Word 的字節(jié)具體是如何分配的
用中文翻譯過來就是
無狀態(tài)也就是無鎖的時候,對象頭開辟 25bit 的空間用來存儲對象的 hashcode ,4bit 用于存放分代年齡,1bit 用來存放是否偏向鎖的標(biāo)識位,2bit 用來存放鎖標(biāo)識位為01
偏向鎖 中劃分更細(xì),還是開辟25bit 的空間,其中23bit 用來存放線程ID,2bit 用來存放 epoch,4bit 存放分代年齡,1bit 存放是否偏向鎖標(biāo)識, 0表示無鎖,1表示偏向鎖,鎖的標(biāo)識位還是01
輕量級鎖中直接開辟 30bit 的空間存放指向棧中鎖記錄的指針,2bit 存放鎖的標(biāo)志位,其標(biāo)志位為00
重量級鎖中和輕量級鎖一樣,30bit 的空間用來存放指向重量級鎖的指針,2bit 存放鎖的標(biāo)識位,為11
GC標(biāo)記開辟30bit 的內(nèi)存空間卻沒有占用,2bit 空間存放鎖標(biāo)志位為11。
其中無鎖和偏向鎖的鎖標(biāo)志位都是01,只是在前面的1bit區(qū)分了這是無鎖狀態(tài)還是偏向鎖狀態(tài)。
關(guān)于為什么這么分配的內(nèi)存,我們可以從 OpenJDK 中的markOop.hpp類中的枚舉窺出端倪
來解釋一下
age_bits 就是我們說的分代回收的標(biāo)識,占用4字節(jié)
lock_bits 是鎖的標(biāo)志位,占用2個字節(jié)
biased_lock_bits 是是否偏向鎖的標(biāo)識,占用1個字節(jié)
max_hash_bits 是針對無鎖計算的hashcode 占用字節(jié)數(shù)量,如果是32位虛擬機(jī),就是 32 - 4 - 2 -1 = 25 byte,如果是64 位虛擬機(jī),64 - 4 - 2 - 1 = 57 byte,但是會有 25 字節(jié)未使用,所以64位的 hashcode 占用 31 byte
hash_bits 是針對 64 位虛擬機(jī)來說,如果大字節(jié)數(shù)大于 31,則取31,否則取真實的字節(jié)數(shù)
cms_bits 我覺得應(yīng)該是不是64位虛擬機(jī)就占用 0 byte,是64位就占用 1byte
epoch_bits 就是 epoch 所占用的字節(jié)大小,2字節(jié)。
Synchronized鎖
synchronized用的鎖是存在Java對象頭里的。
JVM基于進(jìn)入和退出 Monitor 對象來實現(xiàn)方法同步和代碼塊同步。代碼塊同步是使用 monitorenter 和 monitorexit 指令實現(xiàn)的,monitorenter 指令是在編譯后插入到同步代碼塊的開始位置,而 monitorexit 是插入到方法結(jié)束處和異常處。任何對象都有一個 monitor 與之關(guān)聯(lián),當(dāng)且一個 monitor 被持有后,它將處于鎖定狀態(tài)。
根據(jù)虛擬機(jī)規(guī)范的要求,在執(zhí)行 monitorenter 指令時,首先要去嘗試獲取對象的鎖,如果這個對象沒被鎖定,或者當(dāng)前線程已經(jīng)擁有了那個對象的鎖,把鎖的計數(shù)器加1,相應(yīng)地,在執(zhí)行 monitorexit 指令時會將鎖計數(shù)器減1,當(dāng)計數(shù)器被減到0時,鎖就釋放了。如果獲取對象鎖失敗了,那當(dāng)前線程就要阻塞等待,直到對象鎖被另一個線程釋放為止。
Monitor
Synchronized是通過對象內(nèi)部的一個叫做監(jiān)視器鎖(monitor)來實現(xiàn)的,監(jiān)視器鎖本質(zhì)又是依賴于底層的操作系統(tǒng)的 Mutex Lock(互斥鎖)來實現(xiàn)的。而操作系統(tǒng)實現(xiàn)線程之間的切換需要從用戶態(tài)轉(zhuǎn)換到核心態(tài),這個成本非常高,狀態(tài)之間的轉(zhuǎn)換需要相對比較長的時間,這就是為什么 Synchronized 效率低的原因。因此,這種依賴于操作系統(tǒng) Mutex Lock 所實現(xiàn)的鎖我們稱之為重量級鎖。
Java SE 1.6為了減少獲得鎖和釋放鎖帶來的性能消耗,引入了偏向鎖和輕量級鎖:鎖一共有4種狀態(tài),級別從低到高依次是:無鎖狀態(tài)、偏向鎖狀態(tài)、輕量級鎖狀態(tài)和重量級鎖狀態(tài)。鎖可以升級但不能降級。
所以鎖的狀態(tài)總共有四種:無鎖狀態(tài)、偏向鎖、輕量級鎖和重量級鎖。隨著鎖的競爭,鎖可以從偏向鎖升級到輕量級鎖,再升級的重量級鎖(但是鎖的升級是單向的,也就是說只能從低到高升級,不會出現(xiàn)鎖的降級)。JDK 1.6中默認(rèn)是開啟偏向鎖和輕量級鎖的,我們也可以通過-XX:-UseBiasedLocking=false來禁用偏向鎖。
鎖的分類及其解釋
無鎖
無鎖狀態(tài),無鎖即沒有對資源進(jìn)行鎖定,所有的線程都可以對同一個資源進(jìn)行訪問,但是只有一個線程能夠成功修改資源。
無鎖的特點就是在循環(huán)內(nèi)進(jìn)行修改操作,線程會不斷的嘗試修改共享資源,直到能夠成功修改資源并退出,在此過程中沒有出現(xiàn)沖突的發(fā)生,這很像我們在之前文章中介紹的 CAS 實現(xiàn),CAS 的原理和應(yīng)用就是無鎖的實現(xiàn)。無鎖無法全面代替有鎖,但無鎖在某些場合下的性能是非常高的。
偏向鎖
Hotspot 的作者經(jīng)過研究發(fā)現(xiàn),大多數(shù)情況下,鎖不僅不存在多線程競爭,還存在鎖由同一線程多次獲得的情況,偏向鎖就是在這種情況下出現(xiàn)的,它的出現(xiàn)是為了解決只有在一個線程執(zhí)行同步時提高性能。
可以從對象頭的分配中看到,偏向鎖要比無鎖多了線程ID 和 epoch,當(dāng)一個線程訪問同步代碼塊并獲取鎖時,會在對象頭和棧幀的記錄中存儲線程的ID,等到下一次線程在進(jìn)入和退出同步代碼塊時就不需要進(jìn)行 CAS 操作進(jìn)行加鎖和解鎖,只需要簡單判斷一下對象頭的 Mark Word 中是否存儲著指向當(dāng)前線程的線程ID,判斷的標(biāo)志當(dāng)然是根據(jù)鎖的標(biāo)志位來判斷的。
偏向鎖的獲取過程
訪問 Mark Word 中偏向鎖的標(biāo)志是否設(shè)置成 1,鎖的標(biāo)志位是否是 01 --- 確認(rèn)為可偏向狀態(tài)。
如果確認(rèn)為可偏向狀態(tài),判斷當(dāng)前線程id 和 對象頭中存儲的線程 ID 是否一致,如果一致的話,則執(zhí)行步驟5,如果不一致,進(jìn)入步驟3
如果當(dāng)前線程ID 與對象頭中存儲的線程ID 不一致的話,則通過 CAS 操作來競爭獲取鎖。如果競爭成功,則將 Mark Word 中的線程ID 修改為當(dāng)前線程ID,然后執(zhí)行步驟5,如果不一致,則執(zhí)行步驟4
如果 CAS 獲取偏向鎖失敗,則表示有競爭(CAS 獲取偏向鎖失敗則表明至少有其他線程曾經(jīng)獲取過偏向鎖,因為線程不會主動釋放偏向鎖)。當(dāng)?shù)竭_(dá)全局安全點(SafePoint)時,會首先暫停擁有偏向鎖的線程,然后檢查持有偏向鎖的線程是否存活(因為可能持有偏向鎖的線程已經(jīng)執(zhí)行完畢,但是該線程并不會主動去釋放偏向鎖),如果線程不處于活動狀態(tài),則將對象頭置為無鎖狀態(tài)(標(biāo)志位為01),然后重新偏向新的線程;如果線程仍然活著,撤銷偏向鎖后升級到輕量級鎖的狀態(tài)(標(biāo)志位為00),此時輕量級鎖由原持有偏向鎖的線程持有,繼續(xù)執(zhí)行其同步代碼,而正在競爭的線程會進(jìn)入自旋等待獲得該輕量級鎖。
執(zhí)行同步代碼
偏向鎖的釋放過程
偏向鎖的釋放過程可以參考上述的步驟4 ,偏向鎖在遇到其他線程競爭鎖時,持有偏向鎖的線程才會釋放鎖,線程不會主動釋放偏向鎖。偏向鎖的撤銷,需要等待全局安全點(在這個時間點上沒有字節(jié)碼正在執(zhí)行),它會首先暫停擁有偏向鎖的線程,判斷鎖是否處于被鎖定狀態(tài),撤銷偏向鎖后恢復(fù)到未鎖定(標(biāo)志位為01)或輕量級鎖(標(biāo)志位為00)的狀態(tài)。
關(guān)閉偏向鎖
偏向鎖在Java 6 和Java 7 里是默認(rèn)啟用的。由于偏向鎖是為了在只有一個線程執(zhí)行同步塊時提高性能,如果你確定應(yīng)用程序里所有的鎖通常情況下處于競爭狀態(tài),可以通過JVM參數(shù)關(guān)閉偏向鎖:-XX:-UseBiasedLocking=false,那么程序默認(rèn)會進(jìn)入輕量級鎖狀態(tài)。
關(guān)于 epoch
真正理解 epoch 的概念比較復(fù)雜,這里簡單理解,就是 epoch 的值可以作為一種檢測偏向鎖有效性的時間戳
輕量級鎖
輕量級鎖是指當(dāng)前鎖是偏向鎖的時候,被另外的線程所訪問,那么偏向鎖就會升級為輕量級鎖,其他線程會通過自旋的形式嘗試獲取鎖,不會阻塞,從而提高性能。
加鎖過程
在代碼進(jìn)入同步塊的時候,如果同步對象鎖狀態(tài)為無鎖狀態(tài)(鎖標(biāo)志位為 01 狀態(tài),是否為偏向鎖為 0 ),虛擬機(jī)首先將在當(dāng)前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用于存儲鎖對象目前的 Mark Word 的拷貝,然后拷貝對象頭中的 Mark Word 復(fù)制到鎖記錄中。
拷貝成功后,虛擬機(jī)將使用 CAS 操作嘗試將對象的 Mark Word 更新為指向 Lock Record 的指針,并將 Lock Record里的 owner 指針指向?qū)ο蟮?Mark Word。
如果這個更新動作成功了,那么這個線程就擁有了該對象的鎖,并且對象Mark Word的鎖標(biāo)志位設(shè)置為 00 ,表示此對象處于輕量級鎖定狀態(tài)。
如果這個更新操作失敗了,虛擬機(jī)首先會檢查對象的 Mark Word 是否指向當(dāng)前線程的棧幀,如果是就說明當(dāng)前線程已經(jīng)擁有了這個對象的鎖,那就可以直接進(jìn)入同步塊繼續(xù)執(zhí)行。否則說明多個線程競爭鎖,輕量級鎖就要膨脹為重量級鎖,鎖標(biāo)志的狀態(tài)值變?yōu)?10 ,Mark Word中存儲的就是指向重量級鎖(互斥量)的指針,后面等待鎖的線程也要進(jìn)入阻塞狀態(tài)。
重量級鎖
重量級鎖也就是通常說 synchronized 的對象鎖,鎖標(biāo)識位為10,其中指針指向的是 monitor 對象(也稱為管程或監(jiān)視器鎖)的起始地址。每個對象都存在著一個 monitor 與之關(guān)聯(lián),對象與其 monitor 之間的關(guān)系有存在多種實現(xiàn)方式,如 monitor 可以與對象一起創(chuàng)建銷毀或當(dāng)線程試圖獲取對象鎖時自動生成,但當(dāng)一個 monitor 被某個線程持有后,它便處于鎖定狀態(tài)。
上圖簡單描述多線程獲取鎖的過程,當(dāng)多個線程同時訪問一段同步代碼時,首先會進(jìn)入 Entry Set當(dāng)線程獲取到對象的 monitor 后進(jìn)入 The Owner 區(qū)域并把 monitor 中的 owner 變量設(shè)置為當(dāng)前線程,同時 monitor 中的計數(shù)器count 加1,若線程調(diào)用 wait() 方法,將釋放當(dāng)前持有的 monitor,owner變量恢復(fù)為 null,count自減1,同時該線程進(jìn)入 WaitSet 集合中等待被喚醒。若當(dāng)前線程執(zhí)行完畢也將釋放 monitor (鎖)并復(fù)位變量的值,以便其他線程進(jìn)入獲取monitor(鎖)。
由此看來,monitor 對象存在于每個Java對象的對象頭中(存儲的指針的指向),synchronized 鎖便是通過這種方式獲取鎖的,也是為什么Java中任意對象可以作為鎖的原因,同時也是 notify/notifyAll/wait 等方法存在于頂級對象Object中的原因。
創(chuàng)新互聯(lián)www.cdcxhl.cn,專業(yè)提供香港、美國云服務(wù)器,動態(tài)BGP最優(yōu)骨干路由自動選擇,持續(xù)穩(wěn)定高效的網(wǎng)絡(luò)助力業(yè)務(wù)部署。公司持有工信部辦法的idc、isp許可證, 機(jī)房獨(dú)有T級流量清洗系統(tǒng)配攻擊溯源,準(zhǔn)確進(jìn)行流量調(diào)度,確保服務(wù)器高可用性。佳節(jié)活動現(xiàn)已開啟,新人活動云服務(wù)器買多久送多久。
分享文章:詳解Java鎖機(jī)制:看完你就明白的鎖系列之鎖的狀態(tài)-創(chuàng)新互聯(lián)
分享鏈接:http://aaarwkj.com/article4/dppsie.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供品牌網(wǎng)站制作、用戶體驗、ChatGPT、網(wǎng)站建設(shè)、網(wǎng)頁設(shè)計公司、網(wǎng)站制作
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時需注明來源: 創(chuàng)新互聯(lián)
猜你還喜歡下面的內(nèi)容