【併發編程】Java中的鎖升級解析

 目錄

(1)Synchronized 

(2) 鎖優化

(3)鎖升級

1)偏向鎖:

2)偏向鎖的升級

3)輕量級鎖

(4)、鎖粗化

(5)、鎖消除

(6)、鎖膨脹 及鎖升級


參考博客:地址

(1)Synchronized 

    Jvm底層是通過監視器來實現synchronized同步的,監視器即monitor。是每個對象與生俱來的的隱藏字段,使用synchronized時,JVM會根據當前synchronized使用環境,找到對應對象的monitor,再根據monitor的狀態進行加鎖、解鎖判斷。

    同步代碼塊中會使用monitorenter及monitorexit兩個字節碼指令獲取和釋放monitor.如果使用monitorenter進入時monitor爲0,表示該線程可以持有monitor後續代碼,並將monitor加1,如果當前線程已經持有monitor,那麼monitor繼續加1,如果monitor非0,其他線程就會進入阻塞狀態。

  • synchronized 關鍵字之鎖的升級(偏向鎖->輕量級鎖->重量級鎖)
  1. 在幾乎無競爭的條件下, 會使用偏向鎖,
  2. 在輕度競爭的條件下, 會由偏向鎖升級爲輕量級鎖,
  3. 在重度競爭的情況下, 會升級到重量級鎖。

(2)Synchronised和reentrantlock的區別

  1. 原始構成sync是JVM層面的,底層通過monitor監聽monitorentermonitorexit來實現的。Lock是JDK API層面的
  2. 使用方法sync不需要手動釋放鎖,而Lock需要手動釋放。
  3. 是否可中斷sync不可中斷,除非拋出異常或者正常運行完成。Lock是可中斷的,通過調用interrupt()方法。
  4. 是否爲公平鎖sync只能是非公平鎖,而Lock既能是公平鎖,又能是非公平鎖。
  5. 綁定多個條件sync不能,只能隨機喚醒。而Lock可以通過Condition來綁定多個條件,精確喚醒。
  6. synchronized 關鍵字結合 wait()notify()/notifyAll() 方法使用,可以實現等待/通知機制,ReentrantLock 類則需要藉助於 Condition 接口與 newCondition() 方法。Condition 是 JDK1.5 之後纔有的,它具有很好的靈活性,比如可以實現多路通知功能,也就是在一個 Lock 對象中可以創建多個 Condition 實例(即對象監視器),線程對象可以註冊在指定的 Condition 中,從而可以有選擇性的進行線程通知,在調度線程上更加靈活。 在使用 notify()/notifyAll() 方法進行通知時,被通知的線程是由 JVM 選擇的。而 synchronized 關鍵字就相當於整個 Lock 對象中只有一個 Condition 實例,所有的線程都註冊在它一個身上。如果執行 notifyAll() 方法的話,就會通知所有處於等待狀態的線程,這樣會造成很大的效率問題,而 Condition 實例的 signalAll() 方法只會喚醒註冊在該 Condition 實例中的所有等待線程。

增加的新特點:

    ① 等待可中斷;② 可實現公平鎖;③ 可實現選擇性通知(鎖可以綁定多個條件):

(2) 鎖優化

  Java SE1.6裏鎖一共有四種狀態,無鎖狀態,偏向鎖狀態,輕量級鎖狀態和重量級鎖狀態,它會隨着競爭情況逐漸升級。鎖可以升級但不能降級,目的是爲了提高獲得鎖和釋放鎖的效率。

在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(即互斥鎖)進入到阻塞狀態。

(3)鎖升級

鎖的4中狀態:無鎖狀態、偏向鎖狀態、輕量級鎖狀態、重量級鎖狀態(級別從低到高)

1)偏向鎖:

爲什麼要引入偏向鎖?

因爲經過HotSpot的作者大量的研究發現,大多數時候是不存在鎖競爭的,常常是一個線程多次獲得同一個鎖,因此如果每次都要競爭鎖會增大很多沒有必要付出的代價,爲了降低獲取鎖的代價,才引入的偏向鎖。

2)偏向鎖的升級

當線程1訪問代碼塊並獲取鎖對象時,會在java對象頭和棧幀中記錄偏向的鎖的threadID,因爲偏向鎖不會主動釋放鎖,因此以後線程1再次獲取鎖的時候,需要比較當前線程的threadID和Java對象頭中的threadID是否一致,如果一致(還是線程1獲取鎖對象),則無需使用CAS來加鎖、解鎖;如果不一致(其他線程,如線程2要競爭鎖對象,而偏向鎖不會主動釋放因此還是存儲的線程1的threadID),那麼需要查看Java對象頭中記錄的線程1是否存活,如果沒有存活,那麼鎖對象被重置爲無鎖狀態,其它線程(線程2)可以競爭將其設置爲偏向鎖;如果存活,那麼立刻查找該線程(線程1)的棧幀信息,如果還是需要繼續持有這個鎖對象,那麼暫停當前線程1,撤銷偏向鎖,升級爲輕量級鎖,如果線程1 不再使用該鎖對象,那麼將鎖對象狀態設爲無鎖狀態,重新偏向新的線程。

  • 偏向鎖的取消:

偏向鎖是默認開啓的,而且開始時間一般是比應用程序啓動慢幾秒,如果不想有這個延遲,那麼可以使用-XX:BiasedLockingStartUpDelay=0;

如果不想要偏向鎖,那麼可以通過-XX:-UseBiasedLocking = false來設置;

3)輕量級鎖

爲什麼要引入輕量級鎖?

輕量級鎖考慮的是競爭鎖對象的線程不多,而且線程持有鎖的時間也不長的情景。因爲阻塞線程需要CPU從用戶態轉到內核態,代價較大,如果剛剛阻塞不久這個鎖就被釋放了,那這個代價就有點得不償失了,因此這個時候就乾脆不阻塞這個線程,讓它自旋並等待鎖釋放。

  • 輕量級鎖什麼時候升級爲重量級鎖?

線程1獲取輕量級鎖時會先把鎖對象的對象頭MarkWord複製一份到線程1的棧幀中創建的用於存儲鎖記錄的空間(稱爲DisplacedMarkWord),然後使用CAS把對象頭中的內容替換爲線程1存儲的鎖記錄(DisplacedMarkWord)的地址;

如果在線程1複製對象頭的同時(在線程1CAS之前),線程2也準備獲取鎖,複製了對象頭到線程2的鎖記錄空間中,但是在線程2CAS的時候,發現線程1已經把對象頭換了,線程2的CAS失敗,那麼線程2就嘗試使用自旋鎖來等待線程1釋放鎖。

但是如果自旋的時間太長也不行,因爲自旋是要消耗CPU的,因此自旋的次數是有限制的,比如10次或者100次,如果自旋次數到了線程1還沒有釋放鎖,或者線程1還在執行,線程2還在自旋等待,這時又有一個線程3過來競爭這個鎖對象,那麼這個時候輕量級鎖就會膨脹爲重量級鎖。重量級鎖把除了擁有鎖的線程都阻塞,防止CPU空轉。

 

*注意:爲了避免無用的自旋,輕量級鎖一旦膨脹爲重量級鎖就不會再降級爲輕量級鎖了;偏向鎖升級爲輕量級鎖也不能再降級爲偏向鎖。一句話就是鎖可以升級不可以降級,但是偏向鎖狀態可以被重置爲無鎖狀態。

(4)、鎖粗化


   按理來說,同步塊的作用範圍應該儘可能小,僅在共享數據的實際作用域中才進行同步,這樣做的目的是爲了使需要同步的操作數量儘可能縮小,縮短阻塞時間,如果存在鎖競爭,那麼等待鎖的線程也能儘快拿到鎖。 
但是加鎖解鎖也需要消耗資源,如果存在一系列的連續加鎖解鎖操作,可能會導致不必要的性能損耗。 
鎖粗化就是將多個連續的加鎖、解鎖操作連接在一起,擴展成一個範圍更大的鎖,避免頻繁的加鎖解鎖操作
 

(5)、鎖消除

Java虛擬機在JIT編譯時(可以簡單理解爲當某段代碼即將第一次被執行時進行編譯,又稱即時編譯),通過對運行上下文的掃描,經過逃逸分析,去除不可能存在共享資源競爭的鎖,通過這種方式消除沒有必要的鎖,可以節省毫無意義的請求鎖時間

 

(6)、鎖膨脹 及鎖升級

 

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