synchronized的優化

我們知道synchronized同一時刻只能有一個線程獲得對象的monitor監聽器,這有時候就會導致我們的效率大大降低,所以引出了鎖的優化,鎖的優化也就是鎖的幾種狀態,在這之前有兩個概念需要知道:CAS操作和java對象頭。

CAS操作

什麼是CAS操作?

我們平時所說的獲取鎖其實是一種悲觀鎖的操作,假設在訪問臨界區代碼的時候都會有衝突,這就意味着只有一個線程獲取到鎖,而別的線程都會被阻塞。而CAS操作是一種樂觀鎖,它假設不會有 衝突,那麼自然就不會有阻塞的線程,那麼一旦有了衝突有了衝突,又是怎麼處理的呢?無鎖操作是使用CAS(compare and swap)又叫做比較交換來鑑別線程是否出現衝突,出現衝突就重試當前操作直到沒有衝突爲止。

CAS的操作過程

CAS比較交換的過程可以通俗的理解爲CAS(V,O,N),包含三個值分別爲:V 內存地址存放的實際值;O 預期的值(舊值);N 更新的新值。當V和O相同時,也就是說舊值和內存中實際的值相同表明該值沒有被其他線程更改過,即該舊值O就是目前來說最新的值了,自然而然可以將新值N賦值給V。反之,V和O不相同,表明該值已經被其他線程改過了則該舊值O不是最新版本的值了,所以不能將新值N賦給V,返回V即可。當多個線程使用CAS操作一個變量時,只有一個線程會成功,併成功更新,其餘會失敗。失敗的線程會重新嘗試,當然也可以選擇掛起線程。

未優化前和synchronized和CAS的主要區別:

在存在線程競爭的情況下會出現線程阻塞和喚醒鎖帶來的性能問題,因爲這是一種互斥同步(阻塞同步)。而CAS並不是武斷的將線程掛起,當CAS操作失敗後會進行一定的嘗試,而非進行耗時的掛起喚醒的操作,因此也叫做非阻塞同步。

對象頭

對象鎖怎麼理解?無非就是對對象有一個標識,而這個標識就存放在對象頭裏面,java對象頭的Mark Word裏面默認存放的是java對象的Hashcode,在32位JVM Mark Word默認存儲結構爲:
在這裏插入圖片描述

在jdk1.6中,鎖一共有四種狀態,按照級別依次是:無鎖狀態、偏向鎖、輕量級鎖、重量級鎖,並且鎖只能升級不能降級,也就是說,如果你把偏向鎖升級爲輕量級鎖,那麼便不能再降級爲偏向鎖,鎖的這種只能升級不能降級的策略大大提高了對象獲取鎖和釋放鎖的效率

對象頭的Mark Word改爲:
在這裏插入圖片描述

偏向鎖

偏向鎖是鎖的四種狀態中最樂觀的一種鎖,從始至終只有一個線程請求某一把鎖.
舉個例子:

就比如說你在私家莊園裏裝了個紅綠燈,並且莊園裏只有你在開車。偏向鎖的做法便是在紅綠燈處識別來車的車牌號。如果匹配到你的車牌號,那麼直接亮綠燈。

偏向鎖的獲取

當一個線程訪問同步塊並獲取鎖時,會在對象頭和棧幀中的鎖記錄裏存儲鎖偏向的線程ID,以後該線程在進入和退出同步塊時不需要進行CAS操作來加鎖和解鎖,只需簡單地測試一下對象頭的Mark Word裏是否存儲着指向當前線程的偏向鎖。如果測試成功,表示線程已經獲得了鎖。如果測試失敗,則需要再測試一下Mark Word中偏向鎖的標識是否設置成1(表示當前是偏向鎖):如果沒有設置,則使用CAS競爭鎖;如果設置了,則嘗試使用CAS將對象頭的偏向鎖指向當前線程。

偏向鎖的撤銷

偏向鎖使用了一種等到競爭出現纔會釋放鎖的機制,當多個線程同時競爭時,持有偏向鎖的線程纔會釋放鎖。

如圖:
在這裏插入圖片描述
下圖線程1展示了偏向鎖獲取的過程,線程2展示了偏向鎖撤銷的過程:
在這裏插入圖片描述

關閉偏向鎖

偏向鎖在JDK6之後是默認啓用的,但是它在應用程序啓動幾秒鐘之後才激活,如有必要可以使用JVM參數來關閉延遲:-XX:BiasedLockingStartupDelay=0。如果你確定應用程序裏所有的鎖通常情況下處於競爭狀態,可以通過JVM參數關閉偏向鎖:-XX:-UseBiasedLocking=false,那麼程序默認會進入輕量級鎖狀態。

輕量級鎖

多個線程在不同的時間段請求同一把鎖,也就是說沒有鎖競爭。針對這種情況,JVM採用了
輕量級鎖,來避免線程的阻塞以及喚醒

加鎖

線程在執行同步塊之前,JVM會先在當前線程的棧楨中創建用於存儲鎖記錄的空間,並將對象頭中的Mark Word複製到鎖記錄中,官方稱爲Displaced Mark Word。然後線程嘗試使用CAS將對象頭中的Mark Word替換爲指向鎖記錄的指針。如果成功,當前線程獲得鎖,如果失敗,表示其他線程競爭鎖,當前線程便嘗試使用自旋來獲取鎖。

解鎖

輕量級解鎖時,會使用原子的CAS操作將Displaced Mark Word替換回到對象頭,如果成功,則表示沒有競爭發生。如果失敗,表示當前鎖存在競爭,鎖就會膨脹成重量級鎖。下圖是兩個線程同時爭奪鎖,導致鎖膨脹的流程圖。

在這裏插入圖片描述

因爲自旋會消耗CPU,爲了避免無用的自旋(比如獲得鎖的線程被阻塞住了),一旦鎖升級成重量級鎖,就不會再恢復到輕量級鎖狀態。當鎖處於這個狀態下,其他線程試圖獲取鎖時,都會被阻塞住,當持有鎖的線程釋放鎖之後會喚醒這些線程,被喚醒的線程就會進行新一輪的奪鎖之爭。

重量級鎖

重量級鎖是JVM中最爲基礎的鎖實現。在這種狀態下,JVM虛擬機會阻塞加鎖失敗的線程,並且在目標鎖被釋放的時候,喚醒這些線程。Java線程的阻塞以及喚醒,都是依靠操作系統來完成的。舉例來說,對於符合posix接口的操作系統(如macOS和絕大部分的Linux),上述操作通過pthread的互斥鎖(mutex)來實現的。此外,這些操作將涉及系統調用,需要從操作系統的用戶態切換至內核態,其開銷非常之大。爲了儘量避免昂貴的線程阻塞、喚醒操作,JVM會在線程進入阻塞狀態之前,以及被喚醒之後競爭不到鎖的情況下,進入自旋狀態,在處理器上空跑並且輪詢鎖是否被釋放。如果此時鎖恰好被釋放了,那麼當前線程便無須進入阻塞狀態,而是直接獲得這把鎖。

總結

Java虛擬機中synchronized關鍵字的實現,按照代價由高到低可以分爲重量級鎖、輕量鎖和偏向鎖三種。

  1. 重量級鎖會阻塞、喚醒請求加鎖的線程。它針對的是多個線程同時競爭同一把鎖的情況。JVM採用了自適應自旋,來避免線程在面對非常小的synchronized代碼塊時,仍會被阻塞、喚醒的情況。
  2. 輕量級鎖採用CAS操作,將鎖對象的標記字段替換爲一個指針,指向當前線程棧上的一塊空間,存儲着鎖對象原本的標記字段。它針對的是多個線程在不同時間段申請同一把鎖的情況。
  3. 偏向鎖只會在第一次請求時採用CAS操作,在鎖對象的標記字段中記錄下當前線程的地址。在之後的運行過程中,持有該偏向鎖的線程的加鎖操作將直接返回。它針對的是鎖僅會被同一線程持有的情況。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章