第四課-java鎖的使用

java中對鎖的優化

  簡單來說在JVM中monitorenter和monitorexit字節碼依賴於底層的操作系統的Mutex Lock來實現的,但是由於使用Mutex Lock需要將當前線程掛起並從用戶態切換到內核態來執行,這種切換的代價是非常昂貴的;然而在現實中的大部分情況下,同步方法是運行在單線程環境(無鎖競爭環境)如果每次都調用Mutex Lock那麼將嚴重的影響程序的性能。不過在jdk1.6中對鎖的實現引入了大量的優化,如鎖粗化(Lock Coarsening)、鎖消除(Lock Elimination)、輕量級鎖(Lightweight Locking)、偏向鎖(Biased Locking)、適應性自旋(Adaptive Spinning)等技術來減少鎖操作的開銷。

鎖粗化(Lock Coarsening):也就是減少不必要的緊連在一起的unlock,lock操作,將多個連續的鎖擴展成一個範圍更大的鎖。

鎖消除(Lock Elimination):通過運行時JIT編譯器的逃逸分析來消除一些沒有在當前同步塊以外被其他線程共享的數據的鎖保護,通過逃逸分析也可以在線程本地Stack上進行對象空間的分配(同時還可以減少Heap上的垃圾收集開銷)。

輕量級鎖(Lightweight Locking):這種鎖實現的背後基於這樣一種假設,即在真實的情況下我們程序中的大部分同步代碼一般都處於無鎖競爭狀態(即單線程執行環境),在無鎖競爭的情況下完全可以避免調用操作系統層面的重量級互斥鎖,取而代之的是在monitorenter和monitorexit中只需要依靠一條CAS原子指令就可以完成鎖的獲取及釋放。當存在鎖競爭的情況下,執行CAS指令失敗的線程將調用操作系統互斥鎖進入到阻塞狀態,當鎖被釋放的時候被喚醒。

偏向鎖(Biased Locking):是爲了在無鎖競爭的情況下避免在鎖獲取過程中執行不必要的CAS原子指令,因爲CAS原子指令雖然相對於重量級鎖來說開銷比較小但還是存在非常可觀的本地延遲

適應性自旋(Adaptive Spinning):當線程在獲取輕量級鎖的過程中執行CAS操作失敗時,在進入與monitor相關聯的操作系統重量級鎖(mutex semaphore)前會進入忙等待(Spinning)然後再次嘗試,當嘗試一定的次數後如果仍然沒有成功則調用與該monitor關聯的semaphore(即互斥鎖)進入到阻塞狀態。

鎖分離:根據功能進行鎖分離,如ReadWriteLock讀多寫少的情況,可以提高性能

 讀寫分離思想可以延伸,只要操作互不影響,鎖就可以分離LinkedBlockingQueue隊列



在LinkedBlockQueue中存在着2把鎖,這樣可以利用讀和不用整個隊列爭鬥一把鎖,從而提高性能

 

偏向鎖和輕量級鎖的區別

優點:消除數據在無競爭情況下的同步原語,提高性能。偏向鎖與輕量級鎖理念上的區別: 

輕量級鎖:在無競爭的情況下使用CAS操作去消除同步使用的互斥量

偏向鎖:在無競爭的情況下把整個同步都消除掉,連CAS操作都不做了

意義:鎖偏向於第一個獲得它的線程。如果在接下來的執行過程中,該鎖沒有被其他的線程獲取,則持有偏向鎖的線程將永遠不需要再進行同步。

相關參數: 

默認-XX:+UseBiasedLocking=true

-XX:-UseBiasedLocking=false關閉偏向鎖

應用程序啓動幾秒鐘之後才激活

-XX:BiasedLockingStartupDelay = 0關閉延遲

當鎖對象第一次被線程獲取的時候,虛擬機把對象頭中的標誌位設爲“01”,即偏向模式。同時使用CAS操作把獲取到這個鎖的線程的ID記錄在對象的Mark Word之中的偏向線程ID,並將是否偏向鎖的狀態位置置爲1。

如果CAS操作成功,持有偏向鎖的線程以後每次進入這個鎖相關的同步塊時,直接檢查ThreadId是否和自身線程Id一致, 

如果一致,則認爲當前線程已經獲取了鎖,虛擬機就可以不再進行任何同步操作(例如Locking、Unlocking及對Mark Word的Update等)。

當有另外一個線程去嘗試獲取這個鎖時,偏向模式就宣告結束。根據鎖對象目前是否處於被鎖定的狀態,撤銷偏向(Revoke Bias)後恢復到未鎖定(標誌位爲“01”)或輕量級鎖定(標誌位爲“00”)的狀態,後續的同步操作就如上面介紹的輕量級那樣執行。

偏向鎖、輕量級鎖的狀態轉化及對象Mark Word的關係如下圖: 

掛起擁有偏向鎖的線程後,存在競爭則將Mark Word恢復爲輕量級鎖,並將該鎖賦予當前堆棧中最近的一個lock record。

在前面一片文章《JVM內部細節之一:synchronized關鍵字及實現細節》中已經提到過偏向鎖的概念,在理解什麼是偏向鎖前必須先理解什麼是輕量級鎖(Lightweight Locking)。引入偏向鎖是爲了在無多線程競爭的情況下儘量減少不必要的輕量級鎖執行路徑,因爲輕量級鎖的獲取及釋放依賴多次CAS原子指令,而偏向鎖只需要在置換ThreadID的時候依賴一次CAS原子指令(由於一旦出現多線程競爭的情況就必須撤銷偏向鎖,所以偏向鎖的撤銷操作的性能損耗必須小於節省下來的CAS原子指令的性能消耗)。下面看具體細節:

一、對象頭中的Mark Word佈局

 

在上一篇文章中所討論的輕量級鎖中在我參考的Paper中對於重量級鎖的實現並沒有通過狀態位來表現而是直接通過在輕量級鎖的Monitor Record中關聯一個底層操作系統的互斥信號量來實現重量級鎖的操作(並不影響我們理解JVM內部鎖的運作過程),在偏向鎖的處理過程中並不涉及重量級鎖,我們這裏只需要關心biasable和lightweight locked兩種狀態。在JDK1.6以後默認已經開啓了偏向鎖這個優化,我們可以通過在啓動JVM的時候加上-XX:-UseBiasedLocking參數來禁用偏向鎖(在存在大量鎖對象的創建並高度併發的環境下禁用偏向鎖能夠帶來一定的性能優化)。

二、偏向鎖的獲取過程(假設開啓了偏向鎖優化):

(1)初始時對象處於biasable狀態,並且ThreadID爲0即biasable & unbiased狀態(這裏不討論epoch和age)

(2)當一個線程試圖鎖住一個處於biasable & unbiased狀態的對象時,通過一個CAS將自己的ThreadID放置到Mark Word中相應的位置,如果CAS操作成功進入第(3)步否則進入(4)步

(3)當進入到這一步時代表當前沒有鎖競爭,Object繼續保持biasable狀態,但是這時ThreadID字段被設置成了偏向鎖所有者的ID,然後進入到第(6)步

(4)當前線程執行CAS獲取偏向鎖失敗(這一步是偏向鎖的關鍵),表示在該鎖對象上存在競爭並且這個時候另外一個線程獲得偏向鎖所有權。當到達全局安全點(safepoint)時獲得偏向鎖的線程被掛起,並從偏向鎖所有者的私有Monitor Record列表中獲取一個空閒的記錄,並將Object設置爲LightWeight Lock狀態並且Mark Word中的LockRecord指向剛纔持有偏向鎖線程的Monitor record,最後被阻塞在安全點的線程被釋放,進入到輕量級鎖的執行路徑中,同時被撤銷偏向鎖的線程繼續往下執行同步代碼。

(5)當一個線程試圖鎖住一個處於biasable & biased並且ThreadID不等於自己的ID時,這時由於存在鎖競爭必須進入到第(4)步來撤銷偏向鎖。

(6)運行同步代碼塊

二、偏向鎖的解鎖過程:

(1)偏向鎖解鎖過程很簡單,只需要測試下是否Object上的偏向鎖模式是否還存在,如果存在則解鎖成功不需要任何其他額外的操作。

 

輕量級鎖

3.1 加鎖

在代碼進入同步塊的時候,如果此同步對象沒有被鎖定(鎖標誌位爲“01”狀態),虛擬機首先將在當前線程的棧幀中建立一個名爲鎖記錄(Lock Record)的空間,用於存儲鎖記錄目前的Mark Word的拷貝(稱爲Displaced Mark Word) ,拷貝mark word的作用:爲了不想在lock與unlock這種底層操作上再加同步。

修改Object mark word輕量級鎖指針作用:告訴其他線程,該object monitor已被佔用

owner指向object mark word作用:在下面的運行過程中,識別哪個對象被鎖住了

然後,虛擬機將使用CAS操作嘗試將對象的Mark Word更新爲指向Lock Record的指針。如果這個更新動作成功了,那麼這個線程就擁有了該對象的鎖,並且對象Mark Word的鎖標誌位(Mark Word的最後2bit)將轉變爲“00”,即表示此對象處於輕量級鎖定狀態,這時候線程堆棧與對象頭的狀態如下

與加鎖一樣通過CAS操作進行的,如果對象的Mark Word仍然指向着線程的鎖記錄,那就用CAS操作把對象當前的Mark Word和線程中複製的Displaced Mark Word替換回來,如果替換成功,整個同步過程就完成了。如果替換失敗,說明有其他線程嘗試過獲取該鎖,那就要在釋放鎖的同時,喚醒被掛起的線程。

兩個線程同時爭奪鎖,導致鎖膨脹的流程圖

輕量級鎖能提高程序同步性能的依據是“對於絕大部分的鎖,在整個同步週期內都是不存在競爭的”,這是一個經驗數據。如果沒有競爭,輕量級鎖使用CAS操作避免了使用互斥量的開銷,但如果存在鎖競爭,除了互斥量的開銷外,還額外發生了CAS操作,因此在有競爭的情況下,輕量級鎖會比傳統的重量級鎖更慢。

輕量級鎖具體實現:

     一個線程能夠通過兩種方式鎖住一個對象:1、通過膨脹一個處於無鎖狀態(狀態位001)的對象獲得該對象的鎖;2、對象已經處於膨脹狀態(狀態位00)但LockWord指向的monitor record的Owner字段爲NULL,則可以直接通過CAS原子指令嘗試將Owner設置爲自己的標識來獲得鎖。

獲取鎖(monitorenter)的大概過程如下:

(1)當對象處於無鎖狀態時(RecordWord值爲HashCode,狀態位爲001),線程首先從自己的可用moniter record列表中取得一個空閒的moniter record,初始Nest和Owner值分別被預先設置爲1和該線程自己的標識,一旦monitor record準備好然後我們通過CAS原子指令安裝該monitor record的起始地址到對象頭的LockWord字段來膨脹(原文爲inflate,我覺得之所以叫inflate主要是由於當對象被膨脹後擴展了對象的大小;爲了空間效率,將monitor record結構從對象頭中抽出去,當需要的時候纔將該結構attach到對象上,但是和這篇Paper有點互相矛盾,兩種實現方式稍微有點不同)該對象,如果存在其他線程競爭鎖的情況而調用CAS失敗,則只需要簡單的回到monitorenter重新開始獲取鎖的過程即可。

(2)對象已經被膨脹同時Owner中保存的線程標識爲獲取鎖的線程自己,這就是重入(reentrant)鎖的情況,只需要簡單的將Nest加1即可。不需要任何原子操作,效率非常高。

(3)對象已膨脹但Owner的值爲NULL,當一個鎖上存在阻塞或等待的線程同時鎖的前一個擁有者剛釋放鎖時會出現這種狀態,此時多個線程通過CAS原子指令在多線程競爭狀態下試圖將Owner設置爲自己的標識來獲得鎖,競爭失敗的線程在則會進入到第四種情況(4)的執行路徑。

(4)對象處於膨脹狀態同時Owner不爲NULL(被鎖住),在調用操作系統的重量級的互斥鎖之前先自旋一定的次數,當達到一定的次數時如果仍然沒有成功獲得鎖,則開始準備進入阻塞狀態,首先將rfThis的值原子性的加1,由於在加1的過程中可能會被其他線程破壞Object和monitor record之間的關聯,所以在原子性加1後需要再進行一次比較以確保LockWord的值沒有被改變,當發現被改變後則要重新進行monitorenter過程。同時再一次觀察Owner是否爲NULL,如果是則調用CAS參與競爭鎖,鎖競爭失敗則進入到阻塞狀態。

釋放鎖(monitorexit)的大概過程如下:

(1)首先檢查該對象是否處於膨脹狀態並且該線程是這個鎖的擁有者,如果發現不對則拋出異常;

(2)檢查Nest字段是否大於1,如果大於1則簡單的將Nest減1並繼續擁有鎖,如果等於1,則進入到第(3)步;

(3)檢查rfThis是否大於0,設置Owner爲NULL然後喚醒一個正在阻塞或等待的線程再一次試圖獲取鎖,如果等於0則進入到第(4)步

(4)縮小(deflate)一個對象,通過將對象的LockWord置換回原來的HashCode值來解除和monitor record之間的關聯來釋放鎖,同時將monitor record放回到線程是有的可用monitor record列表。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章