需要有個標識或者狀態來表示鎖是否已經被" />
這是我們在探討AQS時候的思考,這里也把它貼過來,是因為同樣是鎖,基本的實現思路都是一樣的,也方便對比兩個鎖的操作,同樣帶著上面的思考,我們來看看synchronized是怎么處理的
從synchronized的使用入手
怎么使用不必多說,本篇就以修飾實例方法中的代碼塊為例來看下它的底層邏輯,如下為代碼塊代碼。
synchronized(this){ }
synchronized是jvm級別實現的,在java端只是一個關鍵字,它在編譯后會變為如下圖的樣子,整個代碼塊前后被monitorenter 和 monitorexit包裹,也就是說synchronized關鍵字的在編譯后就變為上面兩個關鍵字。
圖片
jvm運行代碼的時候,當遇到這兩個關鍵字時候就會做出相應的處理。
其實很簡單,這兩個關鍵字無非是在邏輯前后分別進行搶鎖和釋放鎖,與ReentrantLock的lock和unlock是一個道理。
monitorenter 和 monitorexit這兩個是jvm級別字節碼指令,不難想到jvm在運行代碼的時候,遇到monitorenter關鍵字,一定會啟動搶鎖的邏輯,包括搶鎖,入隊,阻塞;而遇到monitorexit的時候一定會走釋放鎖邏輯,包括釋放鎖,喚醒阻塞線程。
通過synchronized代碼塊的用法我們就能知道java的synchronized使用是依賴一個對象的,所以我們下面先看下java中的對象是怎么創建的。
java中如何創建一個對象?java代碼會被編譯成字節碼然后被jvm運行,jvm在遇到new關鍵字的時候就會啟動對象的創建流程,對象的大致流程如下:
圖片
默認情況下jvm加載類是懶加載的,所以創建對象的第一步是判斷類是否已經加載,如果沒有加載,需要先走類的加載流程。
接下來是分配內存,一個對象在類加載的時候就可以知道所需要的內存大小,此時就是在堆中劃分一塊區域出來作為對象的私密空間,具體如何分配和具體使用的垃圾回收器有關,jvm篇再細講,在偌大的堆中怎么為一個對象劃分區間呢?這里的分配主要是兩種方法:指針碰撞和空閑列表,但是不管哪種劃分方法都會存在并發問題,此時jvm的解決方案是TLAB和cas配合失敗重試。
初始化零值這一步是給對象中的屬性賦零值,比如int類型默認為0,這一步是避免屬性不賦值的情況下出現空指針異常。
每個對象都會有一個對象頭區域,這個區域包括Mark Word,元數據指針,數組長度三個部分,Mark Word用于保存對象的運行時數據,比如hashcode,分代年齡,鎖標識等,元數據指針是當前對象所屬類對象的地址,只有數組對象才會有數組長度。
最后初始化對象,這個時候一個完整的對象生成了。
一個完整對象的結構如下:
圖片
可以看到結構中有一個對齊填充,對齊填充是為了滿足對象的大小為8字節的整數倍,只有8字節的整數倍才是最高效的存取方式。所以一個對象的大小總是8字節的整數倍。
對象頭是對象中用于保存實例數據外的運行時數據的區域。
我們知道java是面向對象的,在java的世界一切皆對象,所以整個jvm的設計都是圍繞對象,包括對象所屬類的加載,對象的創建,對象的保存,對象的銷毀,對象的回收,鎖的實現,以及jvm的內存結構等等都要圍繞對象設計,這就導致對象自身會有很多的運行時數據,比如垃圾回收依賴的分代年齡,代碼運行過程中用于標識對象唯一的hashcode,當用作鎖對象的時候鎖的相關信息存儲,記錄當前對象所屬的類對象指針等等。
所以jvm設計了對象頭,對象頭包括Mark Word,MetaDate,數組長度三部分。
jvm對于鎖的設計是監視器鎖,當對象作為鎖的時候,jvm會為該對象關聯一個Monitor對象(一一對應),這個Monitor對象就是該對象攜帶的一個監視器。
這個Monitor對象是jvm級別實現的,是一個jvm級別的對象,所以我們在java端開發的時候是看不到摸不到的,但卻是真實存在的。
Monitor對象結構如下:
ObjectMonitor() { _header = NULL; _count = 0; // 記錄個數 _waiters = 0, _recursions = 0; _object = NULL; _owner = NULL; // 占用資源的線程 _WaitSet = NULL; // 處于wait狀態的線程,會被加入到_WaitSet _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; FreeNext = NULL ; _EntryList = NULL ; // 處于等待鎖block狀態的線程,會被加入到該列表 _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; }
synchronized鎖在優化前就是一個地地道道的重量級鎖,重量級鎖的實現其實就是依賴于多線程爭奪Monitor鎖的擁有權,而Monitor鎖的實現則依賴于操作系統底層的互斥原語mutex,因此每一次獲取Monitor鎖的時候都會經過用戶態到內核態的切換,性能很低。這便是重量鎖的由來。
上面做了一些相關知識的介紹,可能還比較碎片化,接下來我們就通過加鎖流程將所有信息都串聯起來。
在jdk1.6之前的鎖就是單純的Monitor鎖,所以性能是很差的,1.6開始對鎖做了優化,性能才得到提升。
JVM內置鎖在1.5之后版本做了如下重大的優化,在做了優化后,其性能顯著提高,基本與ReentrantLock保持同等性能。
接下來由淺入深講解優化內容
鎖的消除,顧名思義就是將鎖去除,因為有些場景下鎖是可以去除的
public void sync() { Sync sync=new Sync(); synchronized(sync){ }}
如上這種情況,我們知道進出一個方法就是當前線程棧的入棧出棧,所以方法內部只要不涉及共享資源操作就是線程安全的,如上這段代碼,sync對象聲明在方法內部,其引用是局部變量,是線程獨享資源不是共享資源,是線程獨有資源,隨著出棧發生,對象也就銷毀了,因此此處是可以不用加鎖的,鎖消除優化就是對這種情況進行去除鎖的處理。
jvm如何進行優化的呢?jvm在JIT編譯時(可以簡單理解為當某段代碼即將第一次被執行時進行編譯,又稱即時編譯),通過對運行上下文的掃描,通過逃逸分析判斷方法中的是否存在共享資源,如果無共享資源則去除不存在共享資源競爭的鎖,從而節省請求鎖時間。
典型的案例是StringBuffer的使用,后續會講解。
通過上面我們知道鎖消除依賴逃逸分析,逃逸分析是可以通過jvm參數配置的,如下:
-XX:+DoEscapeAnalysis 開啟逃逸分析
-XX:+EliminateLocks 開啟鎖消除
逃逸分析可以簡單理解為分析資源是否能逃逸到其他方法或者其他線程中。
顧名思義,把小范圍的多個鎖變成大范圍少個鎖。
public Sync sync=new Sync();public void sync() { synchronized(sync){ } synchronized(sync){ } synchronized(sync){ }}
上面的代碼可以看出,一個邏輯被多次加同一把鎖,每一次上鎖都是會耗時的,所以完全可以把多個鎖合并為一個鎖,這樣只需要上一次鎖就可以了,大大節省了時間。
同樣jvm在即時編譯的時候會掃描判斷是否存在可以粗化的鎖行為。
鎖膨脹又叫鎖升級。
鎖升級是鎖優化后的鎖機制,這個機制中包含這樣幾個概念:偏向鎖,輕量級鎖,適應性自旋,重量級鎖。
鎖升級是依靠對象頭的Mark Word來保存標志信息的,接下來以32位操作系統來看下鎖升級過程中的對象頭中運行時數據的變化。
圖片
此時鎖標志位為01,是否偏向為0,代表無鎖狀態。但是此時并不一定有hashcode,因為hashcode是代碼運行過程中調用生成方法才生成的,如果運行過程不調用就不會生成。
請注意,hashcode的生成是會影響鎖的升級過程的。
當第一個線程T1進入代碼塊后的步驟(前提條件是全程無hashcode生成)
此時Mark Word為:23bit的線程id,4bit的分代年齡,是否偏向為1,鎖標識位依然為01。
此時是偏向鎖狀態,它其實是一種特殊的無鎖狀態。
上面的過程是建立在全程無hashcode生成的基礎上,我們知道了hashcode會占用25bit,線程id會占用23bit,如果過程有hashcode生成怎么辦,這里涉及到兩個問題。
第一個問題,T1進入前就已經生成了hashcode怎么處理?
jvm的做法是如果偏向前已經生成hashcode,那么就放棄偏向,直接進入輕量級鎖。
第二個問題,T1進入后鎖狀態變為了偏向鎖,此時生成hashcode怎么處理?
jvm的做法是撤銷偏向,直接進入重量級鎖。
所以我們在使用鎖的時候要特別注意hashcode生成給鎖升級帶來的影響。
當第二個線程T2進入代碼塊后
輕量級鎖是在資源競爭壓力不是很大的情況下,避免每個線程都去獲取鎖而造成用戶態到內核態的切換,這個切換是比較耗時的,這樣就能提高性能,但是如果競爭壓力大的情況下輕量級鎖就不行了,因為壓力大意味著有很多線程在輪序失敗重試獲取輕量級鎖,短時間內會造成cpu壓力飆升,甚至拖垮cpu,這個時候就必須升級為重量級鎖。
那么如何才算競爭壓力大,什么時候會升級為重量級鎖呢?
jvm默認輪詢次數限制值為十次,超過十次獲取不到資源就代表競爭壓力比較大了,用戶也可以使用如下參數配置來自行更改這個次數
-XX:PreBlockSpin
但是有個問題,如果通過這個默認值或者這個jvm參數配置限制數量,那意味著jvm全系統的鎖都要遵循,這個數字可能不適用于所有的鎖,因此jvm引入了自適應的自旋。自適應意味著自旋的時間不再是固定的了,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定的。如果在同一個鎖對象上,自旋等待剛剛成功獲得過鎖,并且持有鎖的線程正在運行中,那么虛擬機就會認為這次自旋也很有可能再次成功,進而允許自旋等待持續相對更長的時間,比如持續100次忙循環。另一方面,如果對于某個鎖,自旋很少成功獲得過鎖,那在以后要獲取這個鎖時將有可能直接省略掉自旋過程,以避免浪費處理器資源。有了自適應自旋,隨著程序運行時間的增長及性能監控信息的不斷完善,虛擬機對程序鎖的狀況預測就會越來越精準,虛擬機就會變得越來越“聰明”了。
重量級鎖就是Monitor鎖,也叫監視器鎖,其實現是依靠操作底層的互斥原語Mutes Lock,因為每一次獲取Monitor鎖都需要用戶態到內核態的切換,所以比較耗時,也就是重量級鎖的由來。當自旋的條件破壞后,比如自旋次數達到限制或者競爭的壓力越來越大,將不再自旋,輕量級鎖升級為重量級鎖,當前對象頭中的Mark Word被復制一份到Monitor對象中,Mark Word中原來的輕量級鎖的鎖記錄指針被換成Monitor對象的指針,然后所有的線程會搶奪Monitor鎖的擁有權,以cas方式將自己的線程id填充到Monitor對象的_owner字段,同時_count字段加1,當然此時能夠cas成功的只會是原來持有輕量級鎖的線程,而那些沒有獲取到Monitor鎖的線程將會被阻塞并放入Monitor對象的_EntryList字段等待喚醒。
此時鎖的標志位為10,表示重量級鎖。
當線程退出Monitor鎖,便會將Monitor鎖中的_count減1,清空_owner,jvm會隨機喚醒_EntryList集合中一個線程重新獲取Monitor鎖,這個隨機便突出了synchronized的不公平性。
本文鏈接:http://www.tebozhan.com/showinfo-26-13525-0.html面試中如何答好:synchronized
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com
上一篇: Java中equals和==的區別