一她倘、前言
鎖的狀態(tài)總共有四種璧微,級別由低到高依次為:無鎖、偏向鎖硬梁、輕量級鎖前硫、重量級鎖,這四種鎖狀態(tài)分別代表什么荧止,為什么會有鎖升級屹电?其實在 JDK 1.6之前,synchronized 還是一個重量級鎖跃巡,是一個效率比較低下的鎖枣察,但是在JDK 1.6后歌殃,Jvm為了提高鎖的獲取與釋放效率對(synchronized )進行了優(yōu)化援所,引入了 偏向鎖 和 輕量級鎖 不狮,從此以后鎖的狀態(tài)就有了四種(無鎖、偏向鎖兔朦、輕量級鎖偷线、重量級鎖),并且四種狀態(tài)會隨著競爭的情況逐漸升級沽甥,而且是不可逆的過程声邦,即不可降級,也就是說只能進行鎖升級(從低級別到高級別)摆舟,不能鎖降級(高級別到低級別)翔忽,意味著偏向鎖升級成輕量級鎖后不能降級成偏向鎖英融。這種鎖升級卻不能降級的策略,目的是為了提高獲得鎖和釋放鎖的效率歇式。
二、鎖的四種狀態(tài)
在 synchronized
最初的實現(xiàn)方式是 “阻塞或喚醒一個Java線程需要操作系統(tǒng)切換CPU狀態(tài)來完成胡野,這種狀態(tài)切換需要耗費處理器時間材失,如果同步代碼塊中內(nèi)容過于簡單,這種切換的時間可能比用戶代碼執(zhí)行的時間還長”硫豆,這種方式就是 synchronized
實現(xiàn)同步最初的方式龙巨,這也是當初開發(fā)者詬病的地方,這也是在JDK6以前 synchronized
效率低下的原因熊响,JDK6中為了減少獲得鎖和釋放鎖帶來的性能消耗旨别,引入了“偏向鎖”和“輕量級鎖”。
所以目前鎖狀態(tài)一種有四種汗茄,從級別由低到高依次是:無鎖秸弛、偏向鎖,輕量級鎖洪碳,重量級鎖递览,鎖狀態(tài)只能升級,不能降級
如圖所示:
三瞳腌、鎖狀態(tài)的思路以及特點
鎖狀態(tài) | 存儲內(nèi)容 | 標志位 |
---|---|---|
無鎖 | 對象的hashCode绞铃、對象分代年齡、是否是偏向鎖(0) | 01 |
偏向鎖 | 偏向線程ID嫂侍、偏向時間戳儿捧、對象分代年齡、是否是偏向鎖(1) | 01 |
輕量級鎖 | 指向棧中鎖記錄的指針 | 00 |
重量級鎖 | 指向互斥量的指針 | 11 |
四挑宠、鎖對比
鎖 | 優(yōu)點 | 缺點 | 適用場景 |
---|---|---|---|
偏向鎖 | 加鎖和解鎖不需要額外的消耗菲盾,和執(zhí)行非同步方法相比僅存在納秒級的差距 | 如果線程間存在鎖競爭,會帶來額外的鎖撤銷的消耗 | 適用于只有一個線程訪問同步塊場景 |
輕量級鎖 | 競爭的線程不會阻塞痹栖,提高了程序的響應速度 | 如果始終得不到索競爭的線程亿汞,使用自旋會消耗CPU | 追求響應速度,同步塊執(zhí)行速度非尘景ⅲ快 |
重量級鎖 | 線程競爭不使用自旋疗我,不會消耗CPU | 線程阻塞,響應時間緩慢 | 追求吞吐量南捂,同步塊執(zhí)行速度較慢 |
五吴裤、Synchronized鎖
synchronized
用的鎖是存在Java對象頭里的,那么什么是對象頭呢溺健?
5.1 Java 對象頭
我們以 Hotspot 虛擬機為例麦牺,Hopspot 對象頭主要包括兩部分數(shù)據(jù):Mark Word(標記字段) 和 Klass Pointer(類型指針)
Mark Word:默認存儲對象的HashCode,分代年齡和鎖標志位信息。這些信息都是與對象自身定義無關(guān)的數(shù)據(jù)剖膳,所以Mark Word被設(shè)計成一個非固定的數(shù)據(jù)結(jié)構(gòu)以便在極小的空間內(nèi)存存儲盡量多的數(shù)據(jù)魏颓。它會根據(jù)對象的狀態(tài)復用自己的存儲空間,也就是說在運行期間Mark Word里存儲的數(shù)據(jù)會隨著鎖標志位的變化而變化吱晒。
Klass Point:對象指向它的類元數(shù)據(jù)的指針甸饱,虛擬機通過這個指針來確定這個對象是哪個類的實例。
在上面中我們知道了仑濒,synchronized
用的鎖是存在Java對象頭里的叹话,那么具體是存在對象頭哪里呢?答案是:存在鎖對象的對象頭的Mark Word中墩瞳,那么MarkWord在對象頭中到底長什么樣驼壶,它到底存儲了什么呢?
在64位的虛擬機中:
在32位的虛擬機中:
下面我們以 32位虛擬機為例喉酌,來看一下其 Mark Word 的字節(jié)具體是如何分配的
無鎖:對象頭開辟 25bit 的空間用來存儲對象的 hashcode 热凹,4bit 用于存放對象分代年齡,1bit 用來存放是否偏向鎖的標識位瞭吃,2bit 用來存放鎖標識位為01
偏向鎖: 在偏向鎖中劃分更細碌嘀,還是開辟 25bit 的空間,其中23bit 用來存放線程ID歪架,2bit 用來存放 Epoch股冗,4bit 存放對象分代年齡,1bit 存放是否偏向鎖標識和蚪, 0表示無鎖止状,1表示偏向鎖,鎖的標識位還是01
輕量級鎖:在輕量級鎖中直接開辟 30bit 的空間存放指向棧中鎖記錄的指針攒霹,2bit 存放鎖的標志位怯疤,其標志位為00
重量級鎖: 在重量級鎖中和輕量級鎖一樣,30bit 的空間用來存放指向重量級鎖的指針催束,2bit 存放鎖的標識位集峦,為11
GC標記: 開辟30bit 的內(nèi)存空間卻沒有占用,2bit 空間存放鎖標志位為11抠刺。
其中無鎖和偏向鎖的鎖標志位都是01塔淤,只是在前面的1bit區(qū)分了這是無鎖狀態(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: 就是我們說的分代回收的標識速妖,占用4字節(jié)
- lock_bits: 是鎖的標志位高蜂,占用2個字節(jié)
- biased_lock_bits: 是是否偏向鎖的標識,占用1個字節(jié)
- max_hash_bits: 是針對無鎖計算的hashcode 占用字節(jié)數(shù)量罕容,如果是32位虛擬機备恤,就是 32 - 4 - 2 -1 = 25 byte稿饰,如果是64 位虛擬機,64 - 4 - 2 - 1 = 57 byte露泊,但是會有 25 字節(jié)未使用喉镰,所以64位的 hashcode 占用 31 byte
- hash_bits: 是針對 64 位虛擬機來說,如果最大字節(jié)數(shù)大于 31滤淳,則取31梧喷,否則取真實的字節(jié)數(shù)
- cms_bits: 不是64位虛擬機就占用 0 byte,是64位就占用 1byte
- epoch_bits: 就是 epoch 所占用的字節(jié)大小脖咐,2字節(jié)。
5.2 Monitor
Monitor 可以理解為一個同步工具或一種同步機制汇歹,通常被描述為一個對象屁擅。每一個 Java 對象就有一把看不見的鎖,稱為內(nèi)部鎖或者 Monitor 鎖产弹。
Monitor 是線程私有的數(shù)據(jù)結(jié)構(gòu)派歌,每一個線程都有一個可用 monitor record 列表,同時還有一個全局的可用列表痰哨。每一個被鎖住的對象都會和一個 monitor 關(guān)聯(lián)胶果,同時 monitor 中有一個 Owner 字段存放擁有該鎖的線程的唯一標識,表示該鎖被這個線程占用斤斧。
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)的鎖我們稱之為重量級鎖。
隨著鎖的競爭烘豌,鎖可以從偏向鎖升級到輕量級鎖载庭,再升級的重量級鎖(但是鎖的升級是單向的,也就是說只能從低到高升級廊佩,不會出現(xiàn)鎖的降級)囚聚。JDK 1.6中默認是開啟偏向鎖和輕量級鎖的,我們也可以通過-XX:-UseBiasedLocking=false來禁用偏向鎖罐寨。
六靡挥、鎖的分類
6.2 無鎖
無鎖是指沒有對資源進行鎖定,所有的線程都能訪問并修改同一個資源鸯绿,但同時只有一個線程能修改成功跋破。
無鎖的特點是修改操作會在循環(huán)內(nèi)進行簸淀,線程會不斷的嘗試修改共享資源。如果沒有沖突就修改成功并退出毒返,否則就會繼續(xù)循環(huán)嘗試租幕。如果有多個線程修改同一個值,必定會有一個線程能修改成功拧簸,而其他修改失敗的線程會不斷重試直到修改成功劲绪。
6.3 偏向鎖
初次執(zhí)行到synchronized代碼塊的時候,鎖對象變成偏向鎖(通過CAS修改對象頭里的鎖標志位)盆赤,字面意思是“偏向于第一個獲得它的線程”的鎖贾富。執(zhí)行完同步代碼塊后,線程并不會主動釋放偏向鎖牺六。當?shù)诙蔚竭_同步代碼塊時颤枪,線程會判斷此時持有鎖的線程是否就是自己(持有鎖的線程ID也在對象頭里),如果是則正常往下執(zhí)行淑际。由于之前沒有釋放鎖畏纲,這里也就不需要重新加鎖。如果自始至終使用鎖的線程只有一個春缕,很明顯偏向鎖幾乎沒有額外開銷盗胀,性能極高。
偏向鎖是指當一段同步代碼一直被同一個線程所訪問時锄贼,即不存在多個線程的競爭時票灰,那么該線程在后續(xù)訪問時便會自動獲得鎖,從而降低獲取鎖帶來的消耗咱娶,即提高性能米间。
當一個線程訪問同步代碼塊并獲取鎖時,會在 Mark Word 里存儲鎖偏向的線程 ID膘侮。在線程進入和退出同步塊時不再通過 CAS 操作來加鎖和解鎖屈糊,而是檢測 Mark Word 里是否存儲著指向當前線程的偏向鎖。輕量級鎖的獲取及釋放依賴多次 CAS 原子指令琼了,而偏向鎖只需要在置換 ThreadID 的時候依賴一次 CAS 原子指令即可逻锐。
偏向鎖只有遇到其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程才會釋放鎖雕薪,線程是不會主動釋放偏向鎖的昧诱。
關(guān)于偏向鎖的撤銷,需要等待全局安全點所袁,即在某個時間點上沒有字節(jié)碼正在執(zhí)行時盏档,它會先暫停擁有偏向鎖的線程,然后判斷鎖對象是否處于被鎖定狀態(tài)燥爷。如果線程不處于活動狀態(tài)蜈亩,則將對象頭設(shè)置成無鎖狀態(tài)懦窘,并撤銷偏向鎖,恢復到無鎖(標志位為01)或輕量級鎖(標志位為00)的狀態(tài)稚配。
6.4 輕量級鎖(自旋鎖)
輕量級鎖是指當鎖是偏向鎖的時候畅涂,卻被另外的線程所訪問,此時偏向鎖就會升級為輕量級鎖道川,其他線程會通過自旋(關(guān)于自旋的介紹見文末)的形式嘗試獲取鎖午衰,線程不會阻塞,從而提高性能冒萄。
輕量級鎖的獲取主要由兩種情況:
① 當關(guān)閉偏向鎖功能時臊岸;
② 由于多個線程競爭偏向鎖導致偏向鎖升級為輕量級鎖。
一旦有第二個線程加入鎖競爭尊流,偏向鎖就升級為輕量級鎖(自旋鎖)扇单。這里要明確一下什么是鎖競爭:如果多個線程輪流獲取一個鎖,但是每次獲取鎖的時候都很順利奠旺,沒有發(fā)生阻塞,那么就不存在鎖競爭施流。只有當某線程嘗試獲取鎖的時候响疚,發(fā)現(xiàn)該鎖已經(jīng)被占用,只能等待其釋放瞪醋,這才發(fā)生了鎖競爭忿晕。
在輕量級鎖狀態(tài)下繼續(xù)鎖競爭,沒有搶到鎖的線程將自旋银受,即不停地循環(huán)判斷鎖是否能夠被成功獲取践盼。獲取鎖的操作,其實就是通過CAS修改對象頭里的鎖標志位宾巍。先比較當前鎖標志位是否為“釋放”咕幻,如果是則將其設(shè)置為“鎖定”,比較并設(shè)置是原子性發(fā)生的顶霞。這就算搶到鎖了肄程,然后線程將當前鎖的持有者信息修改為自己。
長時間的自旋操作是非常消耗資源的选浑,一個線程持有鎖蓝厌,其他線程就只能在原地空耗CPU,執(zhí)行不了任何有效的任務古徒,這種現(xiàn)象叫做忙等(busy-waiting)拓提。如果多個線程用一個鎖,但是沒有發(fā)生鎖競爭隧膘,或者發(fā)生了很輕微的鎖競爭代态,那么synchronized就用輕量級鎖寺惫,允許短時間的忙等現(xiàn)象。這是一種折衷的想法胆数,短時間的忙等肌蜻,換取線程在用戶態(tài)和內(nèi)核態(tài)之間切換的開銷。
6.4 重量級鎖
重量級鎖顯然必尼,此忙等是有限度的(有個計數(shù)器記錄自旋次數(shù)蒋搜,默認允許循環(huán)10次,可以通過虛擬機參數(shù)更改)判莉。如果鎖競爭情況嚴重豆挽,某個達到最大自旋次數(shù)的線程,會將輕量級鎖升級為重量級鎖(依然是CAS修改鎖標志位券盅,但不修改持有鎖的線程ID)帮哈。當后續(xù)線程嘗試獲取鎖時,發(fā)現(xiàn)被占用的鎖是重量級鎖锰镀,則直接將自己掛起(而不是忙等)娘侍,等待將來被喚醒。
重量級鎖是指當有一個線程獲取鎖之后泳炉,其余所有等待獲取該鎖的線程都會處于阻塞狀態(tài)憾筏。
簡言之,就是所有的控制權(quán)都交給了操作系統(tǒng)花鹅,由操作系統(tǒng)來負責線程間的調(diào)度和線程的狀態(tài)變更氧腰。而這樣會出現(xiàn)頻繁地對線程運行狀態(tài)的切換,線程的掛起和喚醒刨肃,從而消耗大量的系統(tǒng)資
五古拴、總結(jié)
文中講述了鎖的四種狀態(tài)以及鎖是如何一步一步升級的過程,文中有理解不到位或者有問題的地方真友,歡迎大家在評論區(qū)中下方指出和交流黄痪,謝謝大家