Java中常用的鎖機制

1.1什麼是鎖?

      在計算機科學中,鎖(lock)或互斥(mutex)是一種同步機制,用於在有許多執行線程的環境中強制對資源的訪問限制。鎖旨在強制實施互斥排他、併發控制策略。

      鎖通常需要硬件支持纔能有效實施。這種支持通常採取一個或多個原子指令的形式,如"test-and-set", "fetch-and-add" or "compare-and-swap"”。這些指令允許單個進程測試鎖是否空閒,如果空閒,則通過單個原子操作獲取鎖。

1.2.鎖的一個重要屬性 粒度 Granularity [grænjʊ‘lærɪtɪ]

在引入鎖粒度之前,需要了解關於鎖的三個概念:

1、鎖開銷 lock overhead 鎖佔用內存空間、 cpu初始化和銷燬鎖、獲取和釋放鎖的時間。程序使用的鎖越多,相應的鎖開銷越大

2、鎖競爭 lock contention 一個進程或線程試圖獲取另一個進程或線程持有的鎖,就會發生鎖競爭。鎖粒度越小,發生鎖競爭的可能性就越小

3、死鎖 deadlock 至少兩個任務中的每一個都等待另一個任務持有的鎖的情況鎖粒度是衡量鎖保護的數據量大小,通常選擇粗粒度的鎖(鎖的數量少,每個鎖保護大量的數據),在當單進程訪問受保護的數據時鎖開銷小,但是當多個進程同時訪問時性能很差。因爲增大了鎖的競爭。相反,使用細粒度的鎖(鎖數量多,每個鎖保護少量的數據)增加了鎖的開銷但是減少了鎖競爭。例如數據庫中,鎖的粒度有表鎖、頁鎖、行鎖、字段鎖、字段的一部分鎖

相關術語  Critical Section(臨界區)、 Mutex/mutual exclusion(互斥體)、 Semaphore/binary semaphore(信號量)

2.鎖的種類

2.1.獨享鎖/共享鎖

獨享鎖是指該鎖一次只能被一個線程所持有。 (ReentrantLock、 Synchronized)

共享鎖是指該鎖可被多個線程所持有。 (ReadWriteLock)

互斥鎖/讀寫鎖

獨享鎖/共享鎖這是廣義上的說法,互斥鎖/讀寫鎖就分別對應具體的實現。在Java中如ReentrantLock就是互斥鎖(獨享鎖), ReadWriteLock就是讀寫鎖(共享鎖)。 獨享鎖與共享鎖也是通過AQS來實現的

鎖升級:讀鎖到寫鎖 (不支持)

鎖降級:寫鎖到讀鎖 (支持)

2.2.讀寫鎖 ReentrantReadWriteLock

高16位代表寫鎖,低16位代表讀鎖


2.2.公平鎖/非公平鎖

公平鎖是指多個線程按照申請鎖的順序來獲取鎖。

非公平鎖是指多個線程獲取鎖的順序並不是按照申請鎖的順序,有可能後申請的線程比先申請的線程優先獲取鎖。有可能會造成飢餓現象。

對於Java ReentrantLock而言,通過構造函數指定該鎖是否是公平鎖,默認是非公平鎖。非公平鎖的優點在於吞吐量比公平鎖大。

對於Synchronized而言,也是一種非公平鎖。由於其並不像ReentrantLock是通過AQS的控制線程對鎖的獲取, 所以並沒有任何辦法使其變成公平鎖。


2.3.可重入鎖

可重入鎖又名遞歸鎖,是指同一個線程在外層方法獲取鎖的時候,在進入內層方法會自動獲取

鎖。

ReentrantLock和Synchronized都是可重入鎖。可重入鎖的一個好處是可一定程度避免死鎖

如上面的代碼,如果synchronized不是可重入鎖的話,testB就不會被當前線程執行,從而形成死鎖。

需要注意的是,可重入鎖加鎖和解鎖的次數要相等。


C==0表明未獲得鎖,Else表示已經獲得鎖,這時對state加1,相應的,每次釋放鎖都會對state減1

2.4.樂觀鎖/悲觀鎖

樂觀鎖/悲觀鎖不是指具體類型的鎖,而是看待併發的角度。

悲觀鎖認爲存在很多併發更新操作,採取加鎖操作,如果不加鎖一定會有問題

樂觀鎖認爲不存在很多的併發更新操作,不需要加鎖。數據庫中樂觀鎖的實現一般採用版本號,Java中可使用CAS實現樂觀鎖。

2.5.分段鎖

分段鎖是一種鎖的設計,並不是一種具體的鎖。對於ConcuttentHashMap就是通過分段鎖實現高效的併發操作。


2.6.自旋鎖

自旋鎖是指嘗試獲取鎖的線程不會阻塞,而是採用循環的方式嘗試獲取鎖。好處是減少上下文切換,缺點是一直佔用CPU資源。


2.7.偏向鎖/輕量級鎖/重量級鎖

這是jdk1.6中對Synchronized鎖做的優化,首先了解下對象頭(Mark Word):

運行時JVM內存佈局


Mark Word在不同鎖狀態下的標誌位存儲


從jdk1.6開始爲了減少獲得鎖和釋放鎖帶來的性能消耗,引入了“偏向鎖”和“輕量級鎖”。鎖共有四種狀態,級別從低到高分別是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態。隨着競爭情況鎖狀態逐漸升級、鎖可以升級但不能降級。

偏向鎖的獲取和撤銷:

HotSpot作者經過研究發現,大多數情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,爲了讓線程獲得鎖的代價更低而引入偏向鎖。

線程1檢查對象頭中的Mark Word中是否存儲了線程1,如果沒有則CAS操作將Mark Word中的線程ID替換爲線程1。此時,鎖偏向線程1,後面該線程進入同步塊時不需要進行CAS操作,只需要簡單的測試一下Mark Word中是否存儲指向當前線程的偏向鎖,如果成功表明該線程已經獲得鎖。如果失敗,則再需要測試一下Mark Word中偏向鎖標識是否設置爲1(是否是偏向鎖),如果沒有設置,則使用CAS競爭鎖,如果設置了,則嘗試使用CAS將偏向鎖指向當前線程

  

偏向鎖的競爭結果:

根據持有偏向鎖的線程是否存活

1.如果不活動,偏向鎖撤銷到無鎖狀態,再偏向到其他線程
2.如果線程仍然活着,則升級到輕量級鎖

偏向鎖在Java6和Java7中默認是開啓的,但是在應用程序啓動幾秒後才激活,如果有必要可以關閉延遲:
-XX:BiasedLockingStartupDelay=0

如果確定應用程序中所有的鎖通常情況下處於競爭狀態,可以通過JVM參數關閉偏向鎖:
-XX:-UseBiasedLocking=false,那麼程序默認會進入輕量級鎖。

-XX:BiasedLockingStartupDelay=0 -XX:+TraceBiasedLocking

輕量級鎖膨脹:

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


偏向鎖、輕量級鎖、重量級鎖的優缺點

1.偏向鎖是爲了避免某個線程反覆獲得/釋放同一把鎖時的性能消耗,如果仍然是同個線程去獲得這個鎖,嘗試偏向鎖時會直接進入同步塊,不需要再次獲得鎖。

2.而輕量級鎖和自旋鎖都是爲了避免直接調用操作系統層面的互斥操作,因爲掛起線程是一個很耗資源的操作。

爲了儘量避免使用重量級鎖(操作系統層面的互斥),首先會嘗試輕量級鎖,輕量級鎖會嘗試使用CAS操作來獲得鎖,如果輕量級鎖獲得失敗,說明存在競爭。但是也許很快就能獲得鎖,就會嘗試自旋鎖,將線程做幾個空循環,每次循環時都不斷嘗試獲得鎖。如果自旋鎖也失敗,那麼只能升級成重量級鎖。

3.可見偏向鎖,輕量級鎖,自旋鎖都是樂觀鎖。

逃逸分析:


逃逸分析:通俗一點講,當一個對象的指針被多個方法或線程引用時,我們稱這個指針發生了逃逸,必須在JIT裏完成

鎖粗化:

如果虛擬機探測到有這樣一串零碎的操作都對同一個對象加鎖,將會把加鎖同步的範圍擴展到整個操作序列的外部,這樣就只需要加鎖一次就夠了

鎖消除:

如果你定義的類的方法上有同步鎖,但在運行時,卻只有一個線程在訪問,此時逃逸分析後的機器碼,會去掉同步鎖運行。

棧上分配:

分析找到未逃逸的變量,將變量類的實例化內存直接在棧裏分配(無需進入堆),分配完成後,繼續在調用棧內執行,最後線程結束,棧空間被回收,局部變量對象也被回收。

從jdk1.6開始默認開啓:
開啓:  -XX:+DoEscapeAnalysis

關閉:  -XX:-DoEscapeAnalysis

3.1.Synchronized與ReentrantLock的區別


從字節碼角度看實例synchronized方法、靜態synchronized方法、synchronized代碼塊實現的不同




ReentrantLock =  一個AQS同步器(維護同步狀態) + 一個AQS同步隊列 + 多個Condition等待隊列

3.2 ReentrantLock繼承體系類圖


ReentrantLock#lock()方法時序圖


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