一、前言
鎖的狀態(tài)總共有四種误澳,級(jí)別由低到高依次為:無(wú)鎖耻矮、偏向鎖、輕量級(jí)鎖忆谓、重量級(jí)鎖裆装,這四種鎖狀態(tài)分別代表什么,為什么會(huì)有鎖升級(jí)倡缠?其實(shí)在 JDK 1.6之前哨免,synchronized 還是一個(gè)重量級(jí)鎖,是一個(gè)效率比較低下的鎖昙沦,但是在JDK 1.6后琢唾,Jvm為了提高鎖的獲取與釋放效率對(duì)(synchronized )進(jìn)行了優(yōu)化,引入了 偏向鎖 和 輕量級(jí)鎖 桅滋,從此以后鎖的狀態(tài)就有了四種(無(wú)鎖慧耍、偏向鎖身辨、輕量級(jí)鎖、重量級(jí)鎖)芍碧,并且四種狀態(tài)會(huì)隨著競(jìng)爭(zhēng)的情況逐漸升級(jí)煌珊,而且是不可逆的過(guò)程,即不可降級(jí)泌豆,也就是說(shuō)只能進(jìn)行鎖升級(jí)(從低級(jí)別到高級(jí)別)定庵,不能鎖降級(jí)(高級(jí)別到低級(jí)別),意味著偏向鎖升級(jí)成輕量級(jí)鎖后不能降級(jí)成偏向鎖踪危。這種鎖升級(jí)卻不能降級(jí)的策略蔬浙,目的是為了提高獲得鎖和釋放鎖的效率。
二贞远、鎖的四種狀態(tài)
在 synchronized 最初的實(shí)現(xiàn)方式是 “阻塞或喚醒一個(gè)Java線程需要操作系統(tǒng)切換CPU狀態(tài)來(lái)完成畴博,這種狀態(tài)切換需要耗費(fèi)處理器時(shí)間,如果同步代碼塊中內(nèi)容過(guò)于簡(jiǎn)單蓝仲,這種切換的時(shí)間可能比用戶代碼執(zhí)行的時(shí)間還長(zhǎng)”俱病,這種方式就是 synchronized實(shí)現(xiàn)同步最初的方式,這也是當(dāng)初開(kāi)發(fā)者詬病的地方袱结,這也是在JDK6以前 synchronized效率低下的原因亮隙,JDK6中為了減少獲得鎖和釋放鎖帶來(lái)的性能消耗,引入了“偏向鎖”和“輕量級(jí)鎖”垢夹。
所以目前鎖狀態(tài)一種有四種溢吻,從級(jí)別由低到高依次是:無(wú)鎖、偏向鎖果元,輕量級(jí)鎖促王,重量級(jí)鎖,鎖狀態(tài)只能升級(jí)而晒,不能降級(jí)
如圖所示:
三硼砰、鎖狀態(tài)的思路以及特點(diǎn)
四、鎖對(duì)比
五欣硼、Synchronized鎖
synchronized 用的鎖是存在Java對(duì)象頭里的题翰,那么什么是對(duì)象頭呢?
5.1 Java 對(duì)象頭
我們以 Hotspot 虛擬機(jī)為例诈胜,Hopspot 對(duì)象頭主要包括兩部分?jǐn)?shù)據(jù):Mark Word(標(biāo)記字段) 和 Klass Pointer(類(lèi)型指針)
Mark Word:默認(rèn)存儲(chǔ)對(duì)象的HashCode豹障,分代年齡和鎖標(biāo)志位信息。這些信息都是與對(duì)象自身定義無(wú)關(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ǔ)空間,也就是說(shuō)在運(yùn)行期間Mark Word里存儲(chǔ)的數(shù)據(jù)會(huì)隨著鎖標(biāo)志位的變化而變化缓熟。
Klass Point:對(duì)象指向它的類(lèi)元數(shù)據(jù)的指針累魔,虛擬機(jī)通過(guò)這個(gè)指針來(lái)確定這個(gè)對(duì)象是哪個(gè)類(lèi)的實(shí)例摔笤。
在上面中我們知道了,synchronized 用的鎖是存在Java對(duì)象頭里的垦写,那么具體是存在對(duì)象頭哪里呢吕世?答案是:存在鎖對(duì)象的對(duì)象頭的Mark Word中,那么MarkWord在對(duì)象頭中到底長(zhǎng)什么樣梯投,它到底存儲(chǔ)了什么呢命辖?
在64位的虛擬機(jī)中:下面我們以 32位虛擬機(jī)為例,來(lái)看一下其 Mark Word 的字節(jié)具體是如何分配的
無(wú)鎖 :對(duì)象頭開(kāi)辟 25bit 的空間用來(lái)存儲(chǔ)對(duì)象的 hashcode 分蓖,4bit 用于存放對(duì)象分代年齡尔艇,1bit 用來(lái)存放是否偏向鎖的標(biāo)識(shí)位,2bit 用來(lái)存放鎖標(biāo)識(shí)位為01
偏向鎖: 在偏向鎖中劃分更細(xì)么鹤,還是開(kāi)辟 25bit 的空間终娃,其中23bit 用來(lái)存放線程ID,2bit 用來(lái)存放 Epoch蒸甜,4bit 存放對(duì)象分代年齡尝抖,1bit 存放是否偏向鎖標(biāo)識(shí), 0表示無(wú)鎖迅皇,1表示偏向鎖,鎖的標(biāo)識(shí)位還是01
輕量級(jí)鎖:在輕量級(jí)鎖中直接開(kāi)辟 30bit 的空間存放指向棧中鎖記錄的指針衙熔,2bit 存放鎖的標(biāo)志位登颓,其標(biāo)志位為00
重量級(jí)鎖: 在重量級(jí)鎖中和輕量級(jí)鎖一樣,30bit 的空間用來(lái)存放指向重量級(jí)鎖的指針红氯,2bit 存放鎖的標(biāo)識(shí)位框咙,為11
GC標(biāo)記: 開(kāi)辟30bit 的內(nèi)存空間卻沒(méi)有占用,2bit 空間存放鎖標(biāo)志位為11痢甘。
其中無(wú)鎖和偏向鎖的鎖標(biāo)志位都是01喇嘱,只是在前面的1bit區(qū)分了這是無(wú)鎖狀態(tài)還是偏向鎖狀態(tài)
關(guān)于內(nèi)存的分配,我們可以在git中openJDK中 markOop.hpp 可以看出:
public:
// Constants
enum { age_bits = 4,
lock_bits = 2,
biased_lock_bits = 1,
max_hash_bits = BitsPerWord - age_bits - lock_bits - biased_lock_bits,
hash_bits = max_hash_bits > 31 ? 31 : max_hash_bits,
cms_bits = LP64_ONLY(1) NOT_LP64(0),
epoch_bits = 2
};
age_bits: 就是我們說(shuō)的分代回收的標(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ì)無(wú)鎖計(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ī)來(lái)說(shuō)如蚜,如果最大字節(jié)數(shù)大于 31压恒,則取31影暴,否則取真實(shí)的字節(jié)數(shù)
cms_bits: 不是64位虛擬機(jī)就占用 0 byte,是64位就占用 1byte
epoch_bits: 就是 epoch 所占用的字節(jié)大小探赫,2字節(jié)型宙。
5.2 Monitor
Monitor 可以理解為一個(gè)同步工具或一種同步機(jī)制,通常被描述為一個(gè)對(duì)象期吓。每一個(gè) Java 對(duì)象就有一把看不見(jiàn)的鎖早歇,稱為內(nèi)部鎖或者 Monitor 鎖。
Monitor 是線程私有的數(shù)據(jù)結(jié)構(gòu)讨勤,每一個(gè)線程都有一個(gè)可用 monitor record 列表箭跳,同時(shí)還有一個(gè)全局的可用列表。每一個(gè)被鎖住的對(duì)象都會(huì)和一個(gè) monitor 關(guān)聯(lián)潭千,同時(shí) monitor 中有一個(gè) Owner 字段存放擁有該鎖的線程的唯一標(biāo)識(shí)谱姓,表示該鎖被這個(gè)線程占用。
Synchronized是通過(guò)對(duì)象內(nèi)部的一個(gè)叫做監(jiān)視器鎖(monitor)來(lái)實(shí)現(xiàn)的刨晴,監(jiān)視器鎖本質(zhì)又是依賴于底層的操作系統(tǒng)的 Mutex Lock(互斥鎖)來(lái)實(shí)現(xiàn)的屉来。而操作系統(tǒng)實(shí)現(xiàn)線程之間的切換需要從用戶態(tài)轉(zhuǎn)換到核心態(tài),這個(gè)成本非常高狈癞,狀態(tài)之間的轉(zhuǎn)換需要相對(duì)比較長(zhǎng)的時(shí)間茄靠,這就是為什么 Synchronized 效率低的原因。因此蝶桶,這種依賴于操作系統(tǒng) Mutex Lock 所實(shí)現(xiàn)的鎖我們稱之為重量級(jí)鎖慨绳。
隨著鎖的競(jìng)爭(zhēng),鎖可以從偏向鎖升級(jí)到輕量級(jí)鎖真竖,再升級(jí)的重量級(jí)鎖(但是鎖的升級(jí)是單向的脐雪,也就是說(shuō)只能從低到高升級(jí),不會(huì)出現(xiàn)鎖的降級(jí))恢共。JDK 1.6中默認(rèn)是開(kāi)啟偏向鎖和輕量級(jí)鎖的战秋,我們也可以通過(guò)-XX:-UseBiasedLocking=false來(lái)禁用偏向鎖。
六讨韭、鎖的分類(lèi)
6.2 無(wú)鎖
無(wú)鎖是指沒(méi)有對(duì)資源進(jìn)行鎖定脂信,所有的線程都能訪問(wèn)并修改同一個(gè)資源,但同時(shí)只有一個(gè)線程能修改成功透硝。
無(wú)鎖的特點(diǎn)是修改操作會(huì)在循環(huán)內(nèi)進(jìn)行吉嚣,線程會(huì)不斷的嘗試修改共享資源。如果沒(méi)有沖突就修改成功并退出蹬铺,否則就會(huì)繼續(xù)循環(huán)嘗試尝哆。如果有多個(gè)線程修改同一個(gè)值,必定會(huì)有一個(gè)線程能修改成功甜攀,而其他修改失敗的線程會(huì)不斷重試直到修改成功秋泄。
6.3 偏向鎖
初次執(zhí)行到synchronized代碼塊的時(shí)候琐馆,鎖對(duì)象變成偏向鎖(通過(guò)CAS修改對(duì)象頭里的鎖標(biāo)志位),字面意思是“偏向于第一個(gè)獲得它的線程”的鎖恒序。執(zhí)行完同步代碼塊后瘦麸,線程并不會(huì)主動(dòng)釋放偏向鎖。當(dāng)?shù)诙蔚竭_(dá)同步代碼塊時(shí)歧胁,線程會(huì)判斷此時(shí)持有鎖的線程是否就是自己(持有鎖的線程ID也在對(duì)象頭里)滋饲,如果是則正常往下執(zhí)行。由于之前沒(méi)有釋放鎖喊巍,這里也就不需要重新加鎖屠缭。如果自始至終使用鎖的線程只有一個(gè),很明顯偏向鎖幾乎沒(méi)有額外開(kāi)銷(xiāo)崭参,性能極高呵曹。
偏向鎖是指當(dāng)一段同步代碼一直被同一個(gè)線程所訪問(wèn)時(shí),即不存在多個(gè)線程的競(jìng)爭(zhēng)時(shí)何暮,那么該線程在后續(xù)訪問(wèn)時(shí)便會(huì)自動(dòng)獲得鎖奄喂,從而降低獲取鎖帶來(lái)的消耗,即提高性能海洼。
當(dāng)一個(gè)線程訪問(wèn)同步代碼塊并獲取鎖時(shí)跨新,會(huì)在 Mark Word 里存儲(chǔ)鎖偏向的線程 ID。在線程進(jìn)入和退出同步塊時(shí)不再通過(guò) CAS 操作來(lái)加鎖和解鎖坏逢,而是檢測(cè) Mark Word 里是否存儲(chǔ)著指向當(dāng)前線程的偏向鎖域帐。輕量級(jí)鎖的獲取及釋放依賴多次 CAS 原子指令,而偏向鎖只需要在置換 ThreadID 的時(shí)候依賴一次 CAS 原子指令即可词疼。
偏向鎖只有遇到其他線程嘗試競(jìng)爭(zhēng)偏向鎖時(shí),持有偏向鎖的線程才會(huì)釋放鎖帘腹,線程是不會(huì)主動(dòng)釋放偏向鎖的贰盗。
關(guān)于偏向鎖的撤銷(xiāo),需要等待全局安全點(diǎn)阳欲,即在某個(gè)時(shí)間點(diǎn)上沒(méi)有字節(jié)碼正在執(zhí)行時(shí)舵盈,它會(huì)先暫停擁有偏向鎖的線程,然后判斷鎖對(duì)象是否處于被鎖定狀態(tài)球化。如果線程不處于活動(dòng)狀態(tài)秽晚,則將對(duì)象頭設(shè)置成無(wú)鎖狀態(tài),并撤銷(xiāo)偏向鎖筒愚,恢復(fù)到無(wú)鎖(標(biāo)志位為01)或輕量級(jí)鎖(標(biāo)志位為00)的狀態(tài)赴蝇。
6.4 輕量級(jí)鎖(自旋鎖)
輕量級(jí)鎖是指當(dāng)鎖是偏向鎖的時(shí)候,卻被另外的線程所訪問(wèn)巢掺,此時(shí)偏向鎖就會(huì)升級(jí)為輕量級(jí)鎖句伶,其他線程會(huì)通過(guò)自旋(關(guān)于自旋的介紹見(jiàn)文末)的形式嘗試獲取鎖劲蜻,線程不會(huì)阻塞,從而提高性能考余。
輕量級(jí)鎖的獲取主要由兩種情況:
① 當(dāng)關(guān)閉偏向鎖功能時(shí)先嬉;
② 由于多個(gè)線程競(jìng)爭(zhēng)偏向鎖導(dǎo)致偏向鎖升級(jí)為輕量級(jí)鎖。
一旦有第二個(gè)線程加入鎖競(jìng)爭(zhēng)楚堤,偏向鎖就升級(jí)為輕量級(jí)鎖(自旋鎖)疫蔓。這里要明確一下什么是鎖競(jìng)爭(zhēng):如果多個(gè)線程輪流獲取一個(gè)鎖,但是每次獲取鎖的時(shí)候都很順利身冬,沒(méi)有發(fā)生阻塞衅胀,那么就不存在鎖競(jìng)爭(zhēng)。只有當(dāng)某線程嘗試獲取鎖的時(shí)候吏恭,發(fā)現(xiàn)該鎖已經(jīng)被占用拗小,只能等待其釋放,這才發(fā)生了鎖競(jìng)爭(zhēng)樱哼。
在輕量級(jí)鎖狀態(tài)下繼續(xù)鎖競(jìng)爭(zhēng)哀九,沒(méi)有搶到鎖的線程將自旋,即不停地循環(huán)判斷鎖是否能夠被成功獲取搅幅。獲取鎖的操作阅束,其實(shí)就是通過(guò)CAS修改對(duì)象頭里的鎖標(biāo)志位。先比較當(dāng)前鎖標(biāo)志位是否為“釋放”茄唐,如果是則將其設(shè)置為“鎖定”息裸,比較并設(shè)置是原子性發(fā)生的。這就算搶到鎖了沪编,然后線程將當(dāng)前鎖的持有者信息修改為自己呼盆。
長(zhǎng)時(shí)間的自旋操作是非常消耗資源的,一個(gè)線程持有鎖蚁廓,其他線程就只能在原地空耗CPU访圃,執(zhí)行不了任何有效的任務(wù),這種現(xiàn)象叫做忙等(busy-waiting)相嵌。如果多個(gè)線程用一個(gè)鎖腿时,但是沒(méi)有發(fā)生鎖競(jìng)爭(zhēng),或者發(fā)生了很輕微的鎖競(jìng)爭(zhēng)饭宾,那么synchronized就用輕量級(jí)鎖批糟,允許短時(shí)間的忙等現(xiàn)象。這是一種折衷的想法看铆,短時(shí)間的忙等徽鼎,換取線程在用戶態(tài)和內(nèi)核態(tài)之間切換的開(kāi)銷(xiāo)。
6.4 重量級(jí)鎖
重量級(jí)鎖顯然,此忙等是有限度的(有個(gè)計(jì)數(shù)器記錄自旋次數(shù)纬傲,默認(rèn)允許循環(huán)10次满败,可以通過(guò)虛擬機(jī)參數(shù)更改)。如果鎖競(jìng)爭(zhēng)情況嚴(yán)重叹括,某個(gè)達(dá)到最大自旋次數(shù)的線程算墨,會(huì)將輕量級(jí)鎖升級(jí)為重量級(jí)鎖(依然是CAS修改鎖標(biāo)志位,但不修改持有鎖的線程ID)汁雷。當(dāng)后續(xù)線程嘗試獲取鎖時(shí)净嘀,發(fā)現(xiàn)被占用的鎖是重量級(jí)鎖,則直接將自己掛起(而不是忙等)侠讯,等待將來(lái)被喚醒挖藏。
重量級(jí)鎖是指當(dāng)有一個(gè)線程獲取鎖之后,其余所有等待獲取該鎖的線程都會(huì)處于阻塞狀態(tài)厢漩。
簡(jiǎn)言之膜眠,就是所有的控制權(quán)都交給了操作系統(tǒng),由操作系統(tǒng)來(lái)負(fù)責(zé)線程間的調(diào)度和線程的狀態(tài)變更溜嗜。而這樣會(huì)出現(xiàn)頻繁地對(duì)線程運(yùn)行狀態(tài)的切換宵膨,線程的掛起和喚醒,從而消耗大量的系統(tǒng)資
五炸宵、總結(jié)
文中講述了鎖的四種狀態(tài)以及鎖是如何一步一步升級(jí)的過(guò)程辟躏,文中有理解不到位或者有問(wèn)題的地方,歡迎大家在評(píng)論區(qū)中下方指出和交流土全,謝謝大家