您好,登錄后才能下訂單哦!
看完你就會(huì)知道,線程如果鎖住了某個(gè)資源,致使其他線程無法訪問的這種鎖被稱為悲觀鎖,相反,線程不鎖住資源的鎖被稱為樂觀鎖,而自旋鎖是基于 CAS 機(jī)制實(shí)現(xiàn)的,CAS又是樂觀鎖的一種實(shí)現(xiàn),那么對(duì)于鎖來說,多個(gè)線程同步訪問某個(gè)資源的流程細(xì)節(jié)是否一樣呢?換句話說,在多線程同步訪問某個(gè)資源時(shí),鎖的狀態(tài)會(huì)如何變化呢?本篇文章來探討一下。
鎖狀態(tài)的分類
Java 語言專門針對(duì) synchronized 關(guān)鍵字設(shè)置了四種狀態(tài),它們分別是:無鎖、偏向鎖、輕量級(jí)鎖和重量級(jí)鎖,但是在了解這些鎖之前還需要先了解一下 Java 對(duì)象頭和 Monitor。
Java 對(duì)象頭
我們知道 synchronized 是悲觀鎖,在操作同步之前需要給資源加鎖,這把鎖就是對(duì)象頭里面的,而Java 對(duì)象頭又是什么呢?我們以 Hotspot 虛擬機(jī)為例,Hopspot 對(duì)象頭主要包括兩部分?jǐn)?shù)據(jù):Mark Word(標(biāo)記字段) 和 Klass Pointer(類型指針)。
Mark Word:默認(rèn)存儲(chǔ)對(duì)象的HashCode,分代年齡和鎖標(biāo)志位信息。這些信息都是與對(duì)象自身定義無關(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í)例。
在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)也就是無鎖的時(shí)候,對(duì)象頭開辟 25bit 的空間用來存儲(chǔ)對(duì)象的 hashcode ,4bit 用于存放分代年齡,1bit 用來存放是否偏向鎖的標(biāo)識(shí)位,2bit 用來存放鎖標(biāo)識(shí)位為01
偏向鎖 中劃分更細(xì),還是開辟25bit 的空間,其中23bit 用來存放線程ID,2bit 用來存放 epoch,4bit 存放分代年齡,1bit 存放是否偏向鎖標(biāo)識(shí), 0表示無鎖,1表示偏向鎖,鎖的標(biāo)識(shí)位還是01
輕量級(jí)鎖中直接開辟 30bit 的空間存放指向棧中鎖記錄的指針,2bit 存放鎖的標(biāo)志位,其標(biāo)志位為00
重量級(jí)鎖中和輕量級(jí)鎖一樣,30bit 的空間用來存放指向重量級(jí)鎖的指針,2bit 存放鎖的標(biāo)識(shí)位,為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)識(shí),占用4字節(jié)
lock_bits 是鎖的標(biāo)志位,占用2個(gè)字節(jié)
biased_lock_bits 是是否偏向鎖的標(biāo)識(shí),占用1個(gè)字節(jié)
max_hash_bits 是針對(duì)無鎖計(jì)算的hashcode 占用字節(jié)數(shù)量,如果是32位虛擬機(jī),就是 32 - 4 - 2 -1 = 25 byte,如果是64 位虛擬機(jī),64 - 4 - 2 - 1 = 57 byte,但是會(huì)有 25 字節(jié)未使用,所以64位的 hashcode 占用 31 byte
hash_bits 是針對(duì) 64 位虛擬機(jī)來說,如果最大字節(jié)數(shù)大于 31,則取31,否則取真實(shí)的字節(jié)數(shù)
cms_bits 我覺得應(yīng)該是不是64位虛擬機(jī)就占用 0 byte,是64位就占用 1byte
epoch_bits 就是 epoch 所占用的字節(jié)大小,2字節(jié)。
Synchronized鎖
synchronized用的鎖是存在Java對(duì)象頭里的。
JVM基于進(jìn)入和退出 Monitor 對(duì)象來實(shí)現(xiàn)方法同步和代碼塊同步。代碼塊同步是使用 monitorenter 和 monitorexit 指令實(shí)現(xiàn)的,monitorenter 指令是在編譯后插入到同步代碼塊的開始位置,而 monitorexit 是插入到方法結(jié)束處和異常處。任何對(duì)象都有一個(gè) monitor 與之關(guān)聯(lián),當(dāng)且一個(gè) monitor 被持有后,它將處于鎖定狀態(tài)。
根據(jù)虛擬機(jī)規(guī)范的要求,在執(zhí)行 monitorenter 指令時(shí),首先要去嘗試獲取對(duì)象的鎖,如果這個(gè)對(duì)象沒被鎖定,或者當(dāng)前線程已經(jīng)擁有了那個(gè)對(duì)象的鎖,把鎖的計(jì)數(shù)器加1,相應(yīng)地,在執(zhí)行 monitorexit 指令時(shí)會(huì)將鎖計(jì)數(shù)器減1,當(dāng)計(jì)數(shù)器被減到0時(shí),鎖就釋放了。如果獲取對(duì)象鎖失敗了,那當(dāng)前線程就要阻塞等待,直到對(duì)象鎖被另一個(gè)線程釋放為止。
Monitor
Synchronized是通過對(duì)象內(nèi)部的一個(gè)叫做監(jiān)視器鎖(monitor)來實(shí)現(xiàn)的,監(jiān)視器鎖本質(zhì)又是依賴于底層的操作系統(tǒng)的 Mutex Lock(互斥鎖)來實(shí)現(xiàn)的。而操作系統(tǒng)實(shí)現(xiàn)線程之間的切換需要從用戶態(tài)轉(zhuǎn)換到核心態(tài),這個(gè)成本非常高,狀態(tài)之間的轉(zhuǎn)換需要相對(duì)比較長的時(shí)間,這就是為什么 Synchronized 效率低的原因。因此,這種依賴于操作系統(tǒng) Mutex Lock 所實(shí)現(xiàn)的鎖我們稱之為重量級(jí)鎖。
Java SE 1.6為了減少獲得鎖和釋放鎖帶來的性能消耗,引入了偏向鎖和輕量級(jí)鎖:鎖一共有4種狀態(tài),級(jí)別從低到高依次是:無鎖狀態(tài)、偏向鎖狀態(tài)、輕量級(jí)鎖狀態(tài)和重量級(jí)鎖狀態(tài)。鎖可以升級(jí)但不能降級(jí)。
所以鎖的狀態(tài)總共有四種:無鎖狀態(tài)、偏向鎖、輕量級(jí)鎖和重量級(jí)鎖。隨著鎖的競(jìng)爭(zhēng),鎖可以從偏向鎖升級(jí)到輕量級(jí)鎖,再升級(jí)的重量級(jí)鎖(但是鎖的升級(jí)是單向的,也就是說只能從低到高升級(jí),不會(huì)出現(xiàn)鎖的降級(jí))。JDK 1.6中默認(rèn)是開啟偏向鎖和輕量級(jí)鎖的,我們也可以通過-XX:-UseBiasedLocking=false來禁用偏向鎖。
鎖的分類及其解釋
無鎖
無鎖狀態(tài),無鎖即沒有對(duì)資源進(jìn)行鎖定,所有的線程都可以對(duì)同一個(gè)資源進(jìn)行訪問,但是只有一個(gè)線程能夠成功修改資源。
無鎖的特點(diǎn)就是在循環(huán)內(nèi)進(jìn)行修改操作,線程會(huì)不斷的嘗試修改共享資源,直到能夠成功修改資源并退出,在此過程中沒有出現(xiàn)沖突的發(fā)生,這很像我們?cè)谥拔恼轮薪榻B的 CAS 實(shí)現(xiàn),CAS 的原理和應(yīng)用就是無鎖的實(shí)現(xiàn)。無鎖無法全面代替有鎖,但無鎖在某些場(chǎng)合下的性能是非常高的。
偏向鎖
Hotspot 的作者經(jīng)過研究發(fā)現(xiàn),大多數(shù)情況下,鎖不僅不存在多線程競(jìng)爭(zhēng),還存在鎖由同一線程多次獲得的情況,偏向鎖就是在這種情況下出現(xiàn)的,它的出現(xiàn)是為了解決只有在一個(gè)線程執(zhí)行同步時(shí)提高性能。
可以從對(duì)象頭的分配中看到,偏向鎖要比無鎖多了線程ID 和 epoch,當(dāng)一個(gè)線程訪問同步代碼塊并獲取鎖時(shí),會(huì)在對(duì)象頭和棧幀的記錄中存儲(chǔ)線程的ID,等到下一次線程在進(jìn)入和退出同步代碼塊時(shí)就不需要進(jìn)行 CAS 操作進(jìn)行加鎖和解鎖,只需要簡單判斷一下對(duì)象頭的 Mark Word 中是否存儲(chǔ)著指向當(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 和 對(duì)象頭中存儲(chǔ)的線程 ID 是否一致,如果一致的話,則執(zhí)行步驟5,如果不一致,進(jìn)入步驟3
如果當(dāng)前線程ID 與對(duì)象頭中存儲(chǔ)的線程ID 不一致的話,則通過 CAS 操作來競(jìng)爭(zhēng)獲取鎖。如果競(jìng)爭(zhēng)成功,則將 Mark Word 中的線程ID 修改為當(dāng)前線程ID,然后執(zhí)行步驟5,如果不一致,則執(zhí)行步驟4
如果 CAS 獲取偏向鎖失敗,則表示有競(jìng)爭(zhēng)(CAS 獲取偏向鎖失敗則表明至少有其他線程曾經(jīng)獲取過偏向鎖,因?yàn)榫€程不會(huì)主動(dòng)釋放偏向鎖)。當(dāng)?shù)竭_(dá)全局安全點(diǎn)(SafePoint)時(shí),會(huì)首先暫停擁有偏向鎖的線程,然后檢查持有偏向鎖的線程是否存活(因?yàn)榭赡艹钟衅蜴i的線程已經(jīng)執(zhí)行完畢,但是該線程并不會(huì)主動(dòng)去釋放偏向鎖),如果線程不處于活動(dòng)狀態(tài),則將對(duì)象頭置為無鎖狀態(tài)(標(biāo)志位為01),然后重新偏向新的線程;如果線程仍然活著,撤銷偏向鎖后升級(jí)到輕量級(jí)鎖的狀態(tài)(標(biāo)志位為00),此時(shí)輕量級(jí)鎖由原持有偏向鎖的線程持有,繼續(xù)執(zhí)行其同步代碼,而正在競(jìng)爭(zhēng)的線程會(huì)進(jìn)入自旋等待獲得該輕量級(jí)鎖。
執(zhí)行同步代碼
偏向鎖的釋放過程
偏向鎖的釋放過程可以參考上述的步驟4 ,偏向鎖在遇到其他線程競(jìng)爭(zhēng)鎖時(shí),持有偏向鎖的線程才會(huì)釋放鎖,線程不會(huì)主動(dòng)釋放偏向鎖。偏向鎖的撤銷,需要等待全局安全點(diǎn)(在這個(gè)時(shí)間點(diǎn)上沒有字節(jié)碼正在執(zhí)行),它會(huì)首先暫停擁有偏向鎖的線程,判斷鎖是否處于被鎖定狀態(tài),撤銷偏向鎖后恢復(fù)到未鎖定(標(biāo)志位為01)或輕量級(jí)鎖(標(biāo)志位為00)的狀態(tài)。
關(guān)閉偏向鎖
偏向鎖在Java 6 和Java 7 里是默認(rèn)啟用的。由于偏向鎖是為了在只有一個(gè)線程執(zhí)行同步塊時(shí)提高性能,如果你確定應(yīng)用程序里所有的鎖通常情況下處于競(jìng)爭(zhēng)狀態(tài),可以通過JVM參數(shù)關(guān)閉偏向鎖:-XX:-UseBiasedLocking=false,那么程序默認(rèn)會(huì)進(jìn)入輕量級(jí)鎖狀態(tài)。
關(guān)于 epoch
真正理解 epoch 的概念比較復(fù)雜,這里簡單理解,就是 epoch 的值可以作為一種檢測(cè)偏向鎖有效性的時(shí)間戳
輕量級(jí)鎖
輕量級(jí)鎖是指當(dāng)前鎖是偏向鎖的時(shí)候,被另外的線程所訪問,那么偏向鎖就會(huì)升級(jí)為輕量級(jí)鎖,其他線程會(huì)通過自旋的形式嘗試獲取鎖,不會(huì)阻塞,從而提高性能。
加鎖過程
在代碼進(jìn)入同步塊的時(shí)候,如果同步對(duì)象鎖狀態(tài)為無鎖狀態(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ū)ο蟮?Mark Word。
如果這個(gè)更新動(dòng)作成功了,那么這個(gè)線程就擁有了該對(duì)象的鎖,并且對(duì)象Mark Word的鎖標(biāo)志位設(shè)置為 00 ,表示此對(duì)象處于輕量級(jí)鎖定狀態(tài)。
如果這個(gè)更新操作失敗了,虛擬機(jī)首先會(huì)檢查對(duì)象的 Mark Word 是否指向當(dāng)前線程的棧幀,如果是就說明當(dāng)前線程已經(jīng)擁有了這個(gè)對(duì)象的鎖,那就可以直接進(jìn)入同步塊繼續(xù)執(zhí)行。否則說明多個(gè)線程競(jìng)爭(zhēng)鎖,輕量級(jí)鎖就要膨脹為重量級(jí)鎖,鎖標(biāo)志的狀態(tài)值變?yōu)?10 ,Mark Word中存儲(chǔ)的就是指向重量級(jí)鎖(互斥量)的指針,后面等待鎖的線程也要進(jìn)入阻塞狀態(tài)。
重量級(jí)鎖
重量級(jí)鎖也就是通常說 synchronized 的對(duì)象鎖,鎖標(biāo)識(shí)位為10,其中指針指向的是 monitor 對(duì)象(也稱為管程或監(jiān)視器鎖)的起始地址。每個(gè)對(duì)象都存在著一個(gè) monitor 與之關(guān)聯(lián),對(duì)象與其 monitor 之間的關(guān)系有存在多種實(shí)現(xiàn)方式,如 monitor 可以與對(duì)象一起創(chuàng)建銷毀或當(dāng)線程試圖獲取對(duì)象鎖時(shí)自動(dòng)生成,但當(dāng)一個(gè) monitor 被某個(gè)線程持有后,它便處于鎖定狀態(tài)。
上圖簡單描述多線程獲取鎖的過程,當(dāng)多個(gè)線程同時(shí)訪問一段同步代碼時(shí),首先會(huì)進(jìn)入 Entry Set當(dāng)線程獲取到對(duì)象的 monitor 后進(jìn)入 The Owner 區(qū)域并把 monitor 中的 owner 變量設(shè)置為當(dāng)前線程,同時(shí) monitor 中的計(jì)數(shù)器count 加1,若線程調(diào)用 wait() 方法,將釋放當(dāng)前持有的 monitor,owner變量恢復(fù)為 null,count自減1,同時(shí)該線程進(jìn)入 WaitSet 集合中等待被喚醒。若當(dāng)前線程執(zhí)行完畢也將釋放 monitor (鎖)并復(fù)位變量的值,以便其他線程進(jìn)入獲取monitor(鎖)。
由此看來,monitor 對(duì)象存在于每個(gè)Java對(duì)象的對(duì)象頭中(存儲(chǔ)的指針的指向),synchronized 鎖便是通過這種方式獲取鎖的,也是為什么Java中任意對(duì)象可以作為鎖的原因,同時(shí)也是 notify/notifyAll/wait 等方法存在于頂級(jí)對(duì)象Object中的原因。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。