【從刷面試題到構建知識體系】Java底層-synchronized鎖-1
- 2019 年 10 月 15 日
- 筆記
在技術論壇中,經常看到一種言論:面試造火箭,幹活擰螺絲。我們平時寫的大部分程式碼的確是CRDU,再提一個層次,也無非就是揉進去複雜一些的業務邏輯,把一堆的CRDU組合起來。
那麼問題來了:我們提倡的研究“底層技術”,難道僅僅是為了面試?或是為了平時碼農們聊天時裝大佬嗎?
當然不是!
小端隨著工作年限的增加,深有感悟:
技術是我們程式設計師的工具箱。
CRDU是我們的默認工具。
平時的點滴積累就是在不斷的豐富自己的工具箱,增加工具種類。
而深挖技術細節,就是在更深入的掌握每一個工具的特性。
在工作中遇到問題時,如果一直使用默認工具,那麼隨著問題域的越來越大,總會遇到捉襟見肘的尷尬。
如果工具箱中不斷的加入得心應手的工具,嗯,辦法就會總比問題多。
以下面即將講的synchronized鎖為例子,如果對它沒有清晰的了解,那麼在解決執行緒安全性問題時,第一反應是盡量避免使用;如果實在避免不掉而用之,也是簡單的模仿,這樣自己其實心裡也是沒底的,也不清楚帶來的開銷有多大影響,甚至不清楚是否能真正解決問題,只能期盼上線後,一切平安…
小端會寫一個系列,以面試中的問題作為切入點(畢竟這種問題涉及到的,是大部分技術人員比較關注,平時也經常使用到的技術),深入技術底層進行分析,搭建自己的知識體系。期望對看到這篇文章的您,有所啟發。
好,下面進入正題。
如果有人讓你講講synchronized的實現細節,那麼,
恭喜你,這是一個能體現技術深度的好機會!
面試官:synchronized關鍵字用過嗎?講講你對他的了解…
有沒有一種被他虐我千百遍的感覺?這個知識點也算是面試中的必現題型了。
不過,以小端的經驗,有的面試官淺嘗輒止,有的面試官則窮追猛打。前一種,可能面試官自己也不太熟悉,我們辛辛苦苦準備的東西,剛開個頭,就被叫停了,不盡興有沒有;而後一種,一般會不斷的深挖,一直問到我們的知識盲區。
1.在第一類面試官面前,我們需要引導,需要爭取足夠的時間把他的知識盲區講清楚,藉此展示出自己的知識深度。
2.在第二類面試官面前,大部分時間是知識體系對等的交流,這就需要我們做到回答時能提綱掣領,一旦深入細節有條理。
那麼問題來了,如何做到呢?
小端利用十一假期,重新梳理和總結相關知識點,試圖勾勒出一個5層金字塔結構(後面稱為S金字塔),來幫助大家構建自有的完整知識體系。
希望在“大場面”中,你也能做到:任你風起雲湧,我自巋然不動!
先放出S金字塔,一睹為快:
拋出大綱很重要
我們大多數人,更習慣於平鋪直敘的方式,描述一個相對複雜的知識結構。
殊不知對於你的聽眾來說,其實這是一種負擔。
對方要全程集中注意力,要在聽後面內容的時候,不斷的重複回憶前面聽到的內容,只有這樣,才能在聽到最後時,構建出相對完整的思維模型,才能方便理解對話的真正含義。
否則就會產生壓迫感,對自己不熟悉的知識更甚【上述第一種面試官】。
這是人的天性使然。
如果我們在對話的剛開始,就先拋出一個簡明的大綱,先幫助對方建立起完整的模型,然後我們再針對每一個分支,做專項描述,讓對方只需要邊聽邊對照大綱印證,減少了中間記憶和回憶的環節,無疑會大大減輕對方的壓力。
所以,針對這個問題,小端一般會首先告訴對方:我將從關鍵字的應用(初入山門)、位元組碼層面的細節(入室弟子)、內部組件(大師兄)、以及組件工作的流程(長老)四個方面來做解釋。
PS:可能有的同學要問了,怎麼少了一層,S塔尖呢?恩…正如字面,我們只是芸芸眾生,這個高度需要潛心研究,還是留給大神以及有志於成為大神的同學吧。
第一層:我們看到的樣子-synchronized關鍵字的應用
這層是基礎。
在多執行緒編程時,synchronized經常被用來解決互斥導致的執行緒安全性問題。用法有兩種個,一種用在方法聲明中:

public synchronized void run() { //... }
View Code
另一種用在方法體程式碼塊中:

1 public void increaseCount() { 2 //…………………… 3 synchronized (this) { 4 for (int i = 0; i < 5; i++) { 5 System.out.println(Thread.currentThread().getName() + ":" + count++); 6 7 } 8 9 }
View Code
如果修飾的是靜態方法,鎖對象是其所在的類對象;如果修飾的是實例方法,鎖對象是當前的實例對象。
第二層:機器看到的樣子-編譯成位元組碼的細節
Java程式碼編譯為位元組碼指令後,方法聲明中的synchronized對應生成ACC_SYNCHRONIZED關鍵字。

1 public synchronized void doSth(); 2 descriptor: ()V 3 flags: ACC_PUBLIC, ACC_SYNCHRONIZED 4 Code: 5 stack=2, locals=1, args_size=1 6 0: ... 7 3:... 8 5: return
View Code
方法體中的synchronized會對應生成monitorenter,monitorexit。

1 public void doSth1(); 2 descriptor: ()V 3 flags: ACC_PUBLIC 4 Code: 5 stack=2, locals=3, args_size=1 6 0: ldc #5 7 2: dup 8 3: astore_1 9 4: monitorenter 10 5: getstatic #2 11 8: ldc #3 12 10: invokevirtual #4 13 13: aload_1 14 14: monitorexit 15 15: goto 23 16 18: astore_2 17 19: aload_1 18 20: monitorexit 19 21: aload_2 20 22: athrow 21 23: return
View Code
當執行到monitorenter關鍵字時,會申請同步鎖;執行到monitorexit關鍵字時,會釋放同步鎖。
這裡需要注意,monitorexit有兩個的作用可以理解為,try…catch…finally,保證在正常執行流程和其他非正常流程時,都能釋放鎖。
第三層:真實的組成:偏向鎖+輕量級鎖+重量級鎖
在傳統重量級鎖模型中,加鎖解鎖是很消耗系統資源的操作。因為加鎖解鎖操作,涉及到執行緒的阻塞和喚醒,而阻塞喚醒,是依靠作業系統來實現的,也就需要程式從用戶態切換到內核態。
在這種情況下,Java虛擬機做出了最直接的優化-自適應自旋。在加鎖失敗、以及被喚醒後未獲取到鎖的時候,進入自旋,以期能在一定的時間內,其他執行緒釋放鎖進而加鎖成功。這是用CPU的消耗來儘可能避免阻塞喚醒操作的初級解決方案。
不過這裡隱含著一個前提:無論任何情況下,我們都認為必須加鎖。至於做的優化,只是在加鎖這件事上,盡量提效而已,也就是50步笑百步的感覺。
不止你有沒有思考過這種問題:我們在寫程式碼的時候是無法確定這段程式碼是否一定存在執行緒安全問題的,那麼我們採取一切從嚴標準寫,也就是明確標識加鎖。然後讓虛擬機在執行時,根據運行時的狀態來決定加鎖不加鎖,豈不是完美?
是的!虛擬機已經做到了:在無執行緒競爭的場景,或者多執行緒近乎於交替執行的場景,是不需要加鎖的(傳統的重量級鎖)。
這也就印證了一種說法,synchronized鎖性能不好,後來經過優化後,性能得到了極大的提升。本質是,在jdk1.6版本中,引入了偏向鎖,輕量級鎖、重量級鎖三層模型.
優化後,虛擬機加鎖的策略,可以簡單描述成:
如果只有一個執行緒調用同步程式碼,顯然沒有必要加鎖。可以通過偏向鎖,只需要一次CAS操作。如果重入,都是一些值比較操作,性能消耗極低。
如果多執行緒近乎於交替執行同步程式碼,僅需要在每次加鎖解鎖時,做CAS修改(其實CAS的主要目的是發現競爭)。
如果的確存在多執行緒競爭情況,再升級為依賴重量級鎖來保障。
第四層:組件間的關係-協同
那麼上述組件是如何協同工作的呢?
可以說很複雜!複雜到小端不得不用獨立一篇來做詳細介紹。