且聽我一個故事講透一個鎖原理之 synchronized
(給 IT一刻鐘 加星標,可以迎娶白富美)
微信公眾號: IT一刻鐘
大型現實非嚴肅主義現場
一刻鐘與你分享優質技術架構與見聞,做一個有劇情的程式設計師
關注可第一時間瞭解更多精彩內容,定期有福利相送喲。
故事從這裡展開
蜀國有一個皇帝叫蜀道難,他比較難伺候,別的皇帝早朝都是在大殿上同時接見所有大臣,共商國是。他不一樣,他說早朝你們不要有事沒事都跑過來嘰嘰喳喳,有事則來,無事則該幹啥幹啥去,然後安排太監每天早上在大門口守著,每次只允許一個大臣進來彙報情況。
“你敢多放進來一個就砍腦袋的幹活。”
太監趕緊下跪,說“謫!”。
第一天,太監傳話欽天監求見,皇帝允了,欽天監上殿報曰:“臣稟報,昨日我司夜觀星象,西方忽現王星忽明忽暗,恐戎狄那邊有亂。”
“朕知道了,退下吧”。一日無事。
第二天,太監傳話欽天監求見,皇帝允了。一日無事。
第三天,太監傳話欽天監求見……一日無事。
第四天,欽天監……一日無事。
第五天,皇帝不耐煩了,和賈太監說,欽天監這老傢伙整天是不是閒著沒事,以後他來了不用給我稟報,直接放他上殿講,講完讓他走吧。
國泰民安的日子依舊過著,每天只有欽天監一個人來報告,賈太監每次看到是欽天監來了,也懶得搭理了,直接放他進去了。 (這就是偏向鎖,稍後我細細道來)
又一日,欽天監如往常進殿報道,賈太監站在門口打著盹,忽然耳邊傳來一個聲音:
“賈太監,幫我稟告聖上,工部李尚書求見。”
“emmm…進去吧…嗯?等等,尚書大人你先等等,欽天監在裡面,你等會再來求見吧。”太監一陣後怕,尋思著欽天監還在裡面呢,這要是放進去了,我這腦袋可就沒了,果然嗜睡誤事。
過了一會兒,李尚書回來詢問求見,被告知欽天監還沒走,只好又離去。
又過了一會兒,李尚書又回來詢問求見,正巧欽天監走了,太監進殿傳話說工部李尚書求見,皇帝宣覲見,李尚書進殿上報了一番東南連連大雨,已派人去監察水利,修繕河堤。 (這就是輕量級鎖)
忽一日,西戎狄和北匈奴同時對帝國西方和北方發難,前線戰事訊息如片片雪花紛紛湧入京城,瞬間殿外來了一群大臣有要事稟告。
一會兒這個來問賈公公我可以進去了嗎?一會兒那個來問賈公公我可以進去了嗎?
把賈太監累的喲,一天下來光說“稍後再來”都把嘴皮子磨破了,沒幾日,賈太監就跪在皇帝面前哭泣道:“聖上啊,快想想辦法呀,奴才這身子骨就要交代在門口了。”
皇帝一聽,說你傻啊,叫他們一個個在門外排隊啊,誰叫你要他們稍後來求見的。
賈太監細思大喜,覺得有理,次日在門口豎起一個牌子“稟報要事者,這邊排隊”,賈太監再也不用一個人對著一群人反覆回話,只需要每次出來一個,然後傳話放進去一個,就可以了。 (這就是重量級鎖)
上面這個故事,分別講述了synchronized內部四種級別的狀態,分別是:
無鎖狀態,偏向鎖狀態,輕量級鎖狀態,重量級鎖狀態。
重量級鎖狀態
我們首先從重量級鎖開始講,重量級鎖是通過 互斥量(Mutex) 來實現的,即一個執行緒進入了synchronized同步塊,在未完成任務時,會阻塞後面的所有執行緒。
就像上面的故事所講的,要稟告要事的大臣只能在大殿門口外一個接一個的阻塞排隊。
之所以稱它為重量級鎖,是因為Java執行緒是對映到作業系統的原生執行緒上的,如果要阻塞或喚醒一個執行緒,都需要依靠作業系統從當前使用者態轉換到核心態中,這種狀態轉換需要耗費處理器很多時間,對於簡單同步塊,可能狀態轉換時間比使用者程式碼執行時間還要長,導致實際業務處理所佔比偏小,效能損失較大。
當然這個在虛擬機器層面進行了一些比如自旋等待,鎖粗化等等的優化,避免陷入頻繁的切換狀態。在這裡我就不細講了,有興趣的可以關注我,我後續再和各位看官講上一講。
輕量級鎖狀態
輕量級鎖是JDK6引入的,它的輕量是相較於通過系統互斥量實現的傳統鎖,輕量鎖並不是用來取代重量級鎖的,而是在沒有大量執行緒競爭的情況下,減少系統互斥量的使用,降低效能的損耗。
輕量級鎖是通過 CAS(Compare And Swap) 機制實現的,即如果鎖被其他執行緒所佔用,當前執行緒會通過自旋來獲取鎖,從而避免使用者態與核心態的轉換。
就像上面故事所說的,大殿中欽天監在彙報工作,工部尚書要求見,並不需要賈太監每次都進去問一下皇帝,惹得皇帝龍顏大怒,而是大臣自己隔一段時間便來詢問賈太監能不能進去,不能就稍後再來問,直到可以進去為止。
偏向鎖狀態
偏向鎖也是JDK6引入的,它存在的依據是“大多數情況下,鎖不僅不存在多執行緒競爭,而且總是由同一執行緒多次獲得”。它是通過 記錄第一次進入同步塊的執行緒id 來實現的,如果下一個要進入同步塊的執行緒與記錄的執行緒id相同,則說明這個鎖由此執行緒佔有,可以直接進入到同步塊,不用執行CAS。
就像故事中的,如果每天只有欽天監一個人來的話,就不用賈太監稟告了,賈太監每次一看到欽天監,尋思著,喲,欽天監呢,您自個兒直接進去吧,說完自個兒出來吧。
如果說輕量鎖是為了消除系統互斥量帶來的效能損耗,那麼偏向鎖就是為了消除CAS帶來的效能損耗,使之在無競爭的情況下消除整個同步,效能無限接近非同步。
如何通過這四種狀態實現效能大幅度提升的
Java物件頭
要說這個問題,我們需要先講一下Java物件頭,每個物件都會有一個物件頭,它分為三個部分:
內容 | 說明 |
---|---|
Mark Word | 儲存物件的hashcode或鎖資訊 |
Class Metadata Address | 儲存到物件型別資料的指標 |
Array length | 陣列的長度(如果當前物件是陣列) |
從表格可見, synchronized鎖的資訊是存在物件頭裡一個叫Mark Word的區域裡的,考慮到虛擬機器的空間效率,Mark Word被設計成非固定的資料結構 ,會根據物件的狀態複用儲存空間來儲存不同的內容:
鎖的升級
當JVM啟用了偏向鎖模式(JDK6以上預設開啟),新建立物件的Mark Word是未鎖定,未偏向但可偏向狀態,此時Mark Word中的Thread id為0,表示未偏向任何執行緒,也叫做匿名偏向(anonymously biased)。
偏向鎖狀態--->無鎖不可偏向狀態/輕量級鎖狀態
當第一個執行緒嘗試進入同步塊時,發現Mark Word中執行緒ID為0,則會使用CAS將自己的執行緒ID設定到Mark Word中,並且,在當前執行緒棧中由高到低順序找到可用的Lock Record,將執行緒ID記錄下。完成這些,此執行緒就獲取了鎖物件的偏向鎖。
當該偏向執行緒再次進入同步塊時,發現鎖物件偏向的就是當前執行緒,會往當前執行緒的棧中新增一條Displaced Mark Word為空的Lock Record中,用來統計重入的次數,然後繼續執行同步塊程式碼,因為執行緒棧是私有的,不需要CAS指令進行操作,所以在偏向鎖模式下,同一個執行緒,只會執行一個CAS,之後獲取釋放鎖只需要對Lock Record做操作,效能損耗基本可以忽略。
當另外一個執行緒試圖進入同步塊時,發現Mark Word中執行緒ID與自己不相符,這個時候就會引發偏向鎖的撤銷,變成無鎖不可偏向狀態或輕量級鎖狀態,當然,這只是巨集觀上的描述,嚴格意義上講是不準確的,因為裡面還存在重偏向機制,這裡就不過於深入,在後續的文章中,我會專門出一篇文章,給各位看官詳細介紹偏向鎖到底是怎麼回事。
無鎖不可偏向狀態--->輕量級鎖狀態
當鎖物件變成無鎖不可偏向狀態時,多個執行緒執行到同步塊以後,會檢查鎖物件狀態值標誌是否加鎖,如果沒有鎖,就把鎖物件的Mark Word資訊拷貝儲存到當前執行緒棧楨中Lock Record裡,然後通過CAS嘗試把物件的Mark Word的值改變成一個指向自己執行緒的指標。如果成功,則當前執行緒獲得鎖物件的輕量級鎖,其他執行緒的CAS就會失敗,因為鎖物件的Mark Word已經變成一個新的指標了,必須等待執行緒釋放鎖,此時其他執行緒則通過自旋來競爭鎖。當獲取鎖的執行緒執行完畢釋放鎖的時候,會將Lock Record裡面之前拷貝的值還原到鎖物件的Mark Word中。
輕量級鎖狀態--->重量級鎖狀態
當自旋次數超過JVM預期上限,會影響效能,所以競爭的執行緒就會把鎖物件的Mark Word指向重鎖,所謂的重鎖,實際上就是一個堆上的monitor物件,即,重量級鎖的狀態下,物件的Mark Word為指向一個堆中monitor物件的指標。
然後所有的競爭執行緒放棄自旋,逐個插入到monitor物件裡的一個佇列尾部,進入阻塞狀態。
當成功獲取輕量級鎖的執行緒執行完畢,嘗試通過CAS釋放鎖時,因為Mark Word已經指向重鎖,導致輕量級鎖釋放失敗,這時執行緒就會知道鎖已經升級為重量級鎖, 它不僅要釋放當前鎖,還要喚醒其他阻塞的執行緒來重新競爭鎖。
大概流程如下圖所示:
這裡有一點需注意的是: 鎖只能升級,不能降級 。
鎖的對比
鎖 | 優點 | 缺點 | 適用場景 |
---|---|---|---|
偏向鎖 | 加鎖和解鎖不需要額外的消耗,和執行非同步方法相比僅存在納秒級的差距 | 如果執行緒間存在鎖競爭,會帶來額外的鎖撤銷的消耗 | 適用於只有一個執行緒訪問同步塊場景 |
輕量級鎖 | 競爭的執行緒不會堵塞,提高了程式的響音速度 | 始終得不到鎖的執行緒,使用自旋會消耗CPU | 追求響應時間,同步塊執行速度非常快 |
重量級鎖 | 執行緒競爭不使用自旋,不會消耗CPU | 執行緒阻塞,響應時間緩慢 | 追求吞吐量,同步塊執行速度較慢 |
synchronized的底層實現
synchronized無非以下兩種:
1.物件鎖:修飾非靜態方法,修飾程式碼塊
2.類鎖:修飾靜態方法,修飾程式碼塊
其中按照修飾型別來分,又可以分為
程式碼塊同步和方法同步
程式碼塊同步
程式碼塊同步鎖的是物件,使用 monitorenter 和 monitorexit 指令實現的。雖然我知道多一行程式碼少一位看官的定理,但是這裡還是必須貼一張程式碼圖,來證明我沒有瞎說,是有理有據的“理據服”。
想要降服妖怪,就得先將其打回原形,所以我們先對一段簡單的程式碼進行反編譯,得到它的位元組碼。
final Object lock = new Object();
public int subtr(int i){
synchronized (lock){
return i-1;
}
}
位元組碼:
可以看出,monitorenter指令是在編譯後插入到同步程式碼塊的開始位置,monitorexit插入到同步程式碼塊結束的地方,正常情況下monitorenter和monitorexit是一對一的匹配,而後面又出現了一個monitorexit,是因為那裡是異常處,用來保證方法執行異常的時候,可以自動釋放鎖,而不會造成死鎖。
方法同步
方法同步的實現官方沒有透露,我們嘗試對一個方法同步的程式碼進行反編譯。
public synchronized int add(int i){
return i+1;
}
位元組碼:
從位元組碼裡也看不到monitorenter和monitorexit,只能發現flags那裡,多了一個 ACC_SYNCHRONIZED 的標示,沒什麼頭緒。不過我猜想,底層應該是鎖方法所屬的物件或類。
這就是synchronized的大致原理,打回原形之後來看,是不是就覺得也不過如此?有什麼疑問或更好的解讀,可以在下方留言,我們進行愉快友好的磋商交流。
如果覺得有用,記得分享~