Java 中的鎖
阻塞鎖、可重入鎖、讀寫鎖、互斥鎖、悲觀鎖、樂觀鎖、公平鎖、偏向鎖、對象鎖、線程鎖、鎖粗化、鎖消除、輕量級鎖、重量級鎖、信號量、獨享鎖、共享鎖、分段鎖java
1、常見的鎖
synchronized 和 Locknode
synchronized 是一個: 非公平、悲觀、獨享、互斥、可重入的輕量級鎖,原生語義上實現的鎖
如下是鎖是在JUC 包,在API層面上的實現
ReentrantLock 是默認非公平但能夠實現公平的、悲觀的、獨享、互斥、可重入、重量級鎖
ReentrantReadWriteLock 它是一個默認非公平但能夠實現公平的,悲觀、寫獨、讀共享、可重入、重量級鎖
1.公平鎖/非公平鎖數組
公平鎖是指多個線程按照申請鎖的順序來獲取鎖。非公平鎖是指多個線程獲取鎖的順序不按申請順序獲取,有可能後申請的線程,先得到鎖。對於ReentrantLock ,經過構造函數指定鎖是否公平鎖,緩存
默認是非公平鎖。非公平鎖的優勢在於吞吐量比公平鎖大。對於 Synchronized 而言,也是一種非公平鎖,因爲並不像ReentrantLock 是經過AQS 來實現線程調度,因此沒有任何辦法變爲公平鎖。安全
2.樂觀鎖/悲觀鎖多線程
悲觀鎖是假設最壞的狀況,每次去拿數據的時候都認爲別人會修改,因此每次在拿數據的時候都會上鎖。別人想拿數據除非得到鎖,好比 synchronized 就是悲觀鎖併發
樂觀鎖:顧名思義就是很樂觀,每次拿數據的時候都認爲別人不會修改數據,因此不會上鎖,可是在更新的時候會判斷一下在此期間別人有沒有更新此數據,可使用版本號等機制。app
樂觀鎖使用於多讀的應用類型,這樣能夠提升吞吐量。好比在 Java 中 jcu.atomic 包下的原子類就是使用了樂觀鎖的一種方式CAS 實現的。函數
疑問:那樂觀鎖基本就和不上鎖有何區別?CAS? 性能
3.獨享鎖/共享鎖
獨享鎖是指鎖只能一次只能被一個線程鎖持有。共享鎖是指該鎖可被多個線程持有。 如 Java ReentrantLock 而言,是獨享鎖,可是對於 Lock 的另外一種實現 ReentrantReadWriteLock 其讀鎖是共享
鎖,寫鎖是獨享鎖。讀鎖的共享保證併發讀的高效,寫的過程是互斥的。獨享鎖於共享鎖也是經過AQS 來實現的,經過不一樣的方法,來實現獨享或者共享。對於synchronized 是獨享鎖。
疑問: AQS ?
4.互斥鎖/讀寫鎖
獨享鎖/共享鎖是一種廣義的說法,互斥鎖/讀寫鎖就是具體的實現。互斥鎖具體的實現 ReentrantLock ,synchronized 讀寫鎖的實現:ReentrantReadWriteLock
5.可重入鎖
可重入鎖又名遞歸鎖,是指在同一個線程在外層方法獲取鎖的時候,在內層方法會自動獲取鎖。
2、synchronized
1.做用
- 原子性:讓線程互斥的訪問同步代碼
- 可見性:保證共享變量的修改可以及時可見
- 有序性:解決重排序問題
2.使用
- 修飾普通方法:監視器鎖monitor 是對象實例 this
- 修飾靜態方法:監視器鎖monitor 對象 Class 實例。由於 Class數據存在永久代,所以靜態方法鎖至關於類的一個全局鎖
- 修飾代碼塊: 監視器monitor 是括號起來的對象實例
public class Test {
public synchronized void test(){ }
public synchronized static void test1(){ }
public void test2(){ synchronized (Test.class) { } } }
3.實現
同步方法: JVM 使用 acc_synchronized 標識來實現。同步代碼塊使用 monitorenter 和 monitorexit 指令實現同步。
同步代碼塊:每一個對象都會與一個 monitor 關聯,當 monitor 被佔用時,就會處於被鎖定狀態,當線程執行到 monitorenter 指令時,就會去嘗試獲取對應的 monitor 。步驟入下
- 每一個 monitor 維護着一個記錄着擁有次數的計數器。未被擁有的 monitor 的該計數器未0,當一個線程得到monitor 後,該計數器變爲1
- 當同一個線程再次得到該 monitor 時,計數器再次自增
- 若是其餘線程已經佔有了 monitor,則該線程進入阻塞狀態,直到monitor 計數器變爲0,再次嘗試獲取 monitor
- 當一個線程釋放 monitor (執行指令monitorexit )時候,計數器自減,當計數器爲0 時,monitor 被釋放,其餘線程能夠得到monitor
同步方法:同步方法是隱式的,一個同步方法會在運行時常量池中的 method_info 結構體中存放 acc_synchronized 標識符。當一個線程訪問方法時,會去檢測是否存在 acc_synchronized 標識。
若是存在,則先要得到對應的 monitor 鎖,而後執行方法。當方法執行結束(正常return 或者 拋出異常)都會釋放對應的 monitor 鎖。若是此時有其餘的線程想要訪問這個方法時,會由於得不到
monitor 鎖而阻塞。
3、鎖優化
jdk 1.6 實現各類鎖優化,如適應性自旋,鎖消除,鎖粗化,輕量級鎖,偏向鎖等,這些技術都是爲了線程之間更高效的共享數據以及解決競爭問題
1.自旋鎖與適應性自旋
在許多應用上,共享數據的鎖狀態只會持續很短的一段時間,爲了這段時間去掛起和恢復線程並不值得(java 線程是映射在內核之上的,線程的掛起和恢復會極大的影響開銷)。若是物理機器上有
一個以上的處理器,能讓2個線程並行執行,可讓後面的請求的線程 「稍等一下」,可是不放棄處理器的執行時間,看看持有鎖的線程是否很快就就放鎖。爲了讓線程等待,咱們須要讓線程執行一個
忙循環(自旋),這項技術就是所謂的自旋鎖。
自旋等待不能代替阻塞,且不說對處理器的要求,自旋等待自己雖然避免了線程切換的開銷,但它要佔用處理器的時間,所以若是鎖被佔用的時間很短,自旋的效果會很好,反之則是性能上的浪費。
所以自旋等待的時間必需要有必定的限度,若是自旋超過了限定的次數仍然沒有成功得到鎖,就應當使用傳統的方式去掛起線程。自旋默認的次數是 10 次
在jdk 1.6 中引入了自適應的自旋鎖,自適應意味着自旋的時間再也不固定,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。若是
在同一個鎖對象上,自旋等待剛剛成功得到過鎖,而且持有鎖的線程正在運行中,那麼虛擬機就會認爲此次自旋頗有可能會成功,就會容許自旋等待相對較長的時間。另外若是不多成功得到過,那麼在
之後要得到這個鎖時將能夠忽略自旋的過程,避免浪費處理器資源。
2.鎖消除
鎖消除是指虛擬機即時編譯器在運行時,對一些代碼上要求同步,可是被檢測到不可能存在共享數據競爭的鎖進行消除。
StringBuffer 的 append 方法用了 synchronized 關鍵字,它是線程安全的。若是在線程方法的局部變量中使用 StringBuffer ,因爲不一樣線程調用方法時都會建立不一樣的對象(在當前線程的虛擬機棧中
建立棧幀),不會出現線程安全是問題,因此append() 不必加鎖,會進行鎖消除。
3.鎖粗化
若是系列的連續操做都對同一個對象反覆加鎖和解鎖,甚至加鎖操做是出如今循環體中的,那即便沒有線程競爭,頻繁的進行互斥同步操做也會致使沒必要要的性能損耗。所以能夠把屢次加鎖的請求
合併成一個請求,以下降短期內大量鎖請求、同步、釋放帶來的損耗。
4.輕量級鎖
輕量級鎖並非用來代替重量級鎖,它的本意是在沒有多線程競爭的前提下,減小傳統的重量級鎖使用操做系統互斥變量產生的性能消耗。
對象頭
HotSpot 虛擬機的對象頭分爲兩部分信息。第一部分用於存儲對象自身的運行時數據,如哈希碼(HashCode) 、GC分代年齡(Generational GC age) 等,官方稱 「Mark Word " . 它是實現輕量級鎖
和偏向鎖的關鍵。另一部分用於存儲指向方法區對象類型數據的指針。若是是數組對象,會有額外的部分存儲數組的長度。
輕量級鎖:在代碼進入同步的時候,若是此同步對象沒有被鎖定(鎖標誌位爲「01「 狀態),虛擬機首先將在當前線程的棧幀中創建一個名爲 鎖記錄的空間,
用於存儲鎖對象目前的 Mark Word 的拷貝。
而後,虛擬機將使用CAS 操做嘗試將對象的 Mark Word 更新爲指向 Lock Record 的指針。若是這個更新動做成功了,那麼這個線程就擁有了該對象的鎖,而且對象 Mark Word 的鎖標誌將轉變
爲 」00「 ,即標識此對象處於輕量級鎖定的狀態。
若是這個更新操做失敗了,虛擬機首先會檢查對象的 Mark Word 是否指向當前線程的棧幀。若是是說明當前線程已經擁有該對象的鎖,那就能夠直接進入同步塊繼續執行,不然說明這個鎖對象已經被其餘線程搶佔了。若是有兩條以上的線程爭用同一個鎖,(自旋失敗後)那輕量級鎖就再也不有效,要膨脹爲重量級鎮,鎖標誌的狀態值變爲 」10「 ,Mark Word 中存儲的就是指向重量級鎖(互斥量)的指針,後面等待鎖的線程也要進入阻寒狀態。
它的解鎖過程也是經過 CAS 操做來進行的,若是對象的 Mak Word 仍然指向着線程的鎖記錄,那就用 CAS 操做把對象當前的 Mark Word 和線程中複製的Displaced Mark Word 替換回來,若是替換成功整個同步過程就完成了。若是替換失敗,說明有其餘線程嘗試過獲取該鎖,那就要在釋放鎖的同時,喚醒被掛起的線程。
若是沒有競爭,輕量級鎖使用 CAS 操做避免了使用互斥量的開銷,但若是存在鎖競爭,除了互斥量的開銷外,還額外發生了CAS操做,所以在有競爭的狀況下,輕量級鎖會比傳統的重量級鎖更慢。
5.偏向鎖
偏向鎖的目的是消除數據在無競爭狀況下的同步原語,進一步提升程序的運行性能。若是說輕量級鎖是在無競爭的狀況下使用 CAS 操做去消除同步使用的互斥量,那偏向鎖就是在無競爭的狀況下把整個同步都清除掉,連CAS操做都不作了。
偏向鎖會偏向於第一個得到它的線程,若是在接下來的執行過程當中,該鎖沒有被其餘的線程獲取,則持有偏向鎖的線程將永遠不須要再進行同步。
假設當前虛擬機啓用了偏向鎖,那麼,當鎖對象第一次被線程獲取的時候,虛擬機將會把對象頭中的標誌位設爲 「01」, 即偏向模式。同時使用 CAS 操做把獲取到這個鎖的線程的 ID 記錄在對象的Mark Word之中,若是CAS操做成功,持有偏向鎖的線程之後每次進入這個鎖相關的同步塊時,虛擬機均可以再也不進行任何同步操做。
當有另一個線程去嘗試獲取這個鎖時,偏向模式就宜告結束。根據鎖對象目前是否處於被鎖定的狀態,撤銷偏向後恢復到未鎖定(標誌位爲「01」)或輕量級鎖定(標誌位爲「00」)的狀態,後續同步操做同輕量級鎖那樣執行。
6.在 synchronized 鎖流程以下
檢測 Mark Word 裏面是否是當前線程的ID,若是是,表示當前線程處於偏向鎖。
若是不是,則使用CAS將當前線程的 ID 替換 Mard Word,若是成功則表示當前線程得到偏向鎖,置偏向標誌位1
若是失敗,則說明發生競爭,撤銷偏向鎖,進而升級爲輕量級鎖。
當前線程使用CAS將對象頭的Mark Word替換爲鎖記錄指針,若是成功,當前線程得到鎖,若是失敗,表示其餘線程競爭鎖,當前線程便嘗試使用自旋來獲取鎖。
若是自旋成功則依然處於輕量級狀態。若是自旋失敗,則升級爲重量級鎖。
7.幾種鎖的對比
4、Lock 鎖
1.Lock 與 synchronized 的不一樣
- Lock 是一個接口,而 synchronized 是 Java 關鍵字,synchronized 是內置的語言實現(虛擬機級別),lock 是經過代碼實現的(API)級別
- synchronized 發生異常,會自動釋放線程佔有的鎖。而 Lock 在發生異常時,若是沒有主動 unlock() 釋放,則極可能形成死鎖現象,所以使用Lock 時,須要在 finally 塊中釋放鎖
- Lock 可讓等待鎖的線程響應中斷,線程能夠中斷幹別的事物,而synchronized 不行,會一直等待下去
2.主要是實現類
ReentrantLock
此類中有3個內部類,分別是 Sync 抽象同步器,NonfairSync 非公平鎖同步器、FairSync 公平同步器
abstract static class Sync extends AbstractQueuedSynchronizer {...} static final class NonfairSync extends Sync{...} static final class FairSync extends Sync {...}
Reentrant.lock() 方法的調用過程
默認非公平
公平鎖加鎖過程
首先公平鎖對應的是 ReentrantLock 內部靜態類 FairSync
1.加鎖時會先從 lock 方法中獲取鎖,調用 AQS 中的 acquire() 方法
final void lock() { acquire(1); }
2.acquire() 方法調用了 tryAcquire() 【FairSync 實現】方法
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
3.tryAcquire() 方法經過 getState() 獲取當前同步狀態,若是 state 爲 0,則經過 CAS 設置該狀態值,state 初始值爲1,設置鎖的擁有者爲當前線程,tryAcquire返回true,不然返回false。若是同一個線程在獲取了鎖以後,再次去獲取了同一個鎖,狀態值就加 1,釋放一次鎖狀態值就減一,這就是可重入鎖。只有線程 A 把此鎖所有釋放了,狀態值減到0,其餘線程纔有機會獲取鎖。
4.若是獲取鎖失敗,也就是 tryAcquire 返回 false,則調用的 addWaiter(Node mode) 方法把該線程包裝成一個 node 節點入同步隊列(FIFO),即嘗試經過 CAS 把該節點追加到隊尾,若是修改失敗,意味着有併發,同步器經過進入 enq 以死循環的方式來保證節點的正確添加,只有經過 CAS 將節點設置成爲尾節點以後,當前線程才能從該方法中返回,不然當前線程不斷的嘗試設置。加入隊列時,先去判斷這個隊列是否是已經初始化了,沒有初始化,則先初始化,生成一個空的頭節點,而後纔是線程節點。
5.加入了同步隊列的線程,經過acquireQueued方法把已經追加到隊列的線程節點進行阻塞,但阻塞前又經過 tryAccquire 重試是否能得到鎖,若是重試成功能則無需阻塞)
6.頭節點在釋放同步狀態的時候,會調用unlock(),而unlock會調用release(),release() 會調用 tryRelease 方法嘗試釋放當前線程持有的鎖(同步狀態),成功的話調用unparkSuccessor() 喚醒後繼線程,並返回true,不然直接返回false
注意:隊列中的節點在被喚醒以前都處於阻塞狀態。當一個線程節點被喚醒而後取得了鎖,對應節點會從隊列中刪除
非公平鎖加鎖過程
1.加鎖時會先從 lock 方法中去獲取鎖,不一樣的是,它的 lock 方法是先直接 CAS 設置 state 變量,若是設置成功,代表加鎖成功。設置失敗,再調用 acquire 方法將線程置於隊列尾部排隊。也是去獲取鎖調用 acquire() 方法,acquire 方法內部一樣調用了 tryAcquire() 方法,nonfairTryAcquire() 方法比公平鎖的 tryAcquire 的if判斷少了一個 !hasQueuedPredecessors()
hasQueuedPredecessors():判斷是否有其餘線程比當前線程在同步隊列中等待的時間更長。有的話,返回 true,不然返回 false,進入隊列中會有一個隊列可能會有多個正在等待的獲取鎖的線程節點,可能有Head(頭結點)、Node一、Node二、Node三、Tail(尾節點),若是此時Node2節點想要去獲取鎖,在公平鎖中他就會先去判斷整個隊列中是否是還有比我等待時間更長的節點,若是有,就讓他先獲取鎖,若是沒有,那我就獲取鎖(這裏就體會到了公平性)
其餘步驟和公平鎖一致
非公平鎖的機制:若是新來了一個線程,試圖訪問一個同步資源,只須要確認當前沒有其餘線程持有這個同步狀態,便可獲取到。
公平鎖的機制:既須要確認當前沒有其餘線程持有這個同步狀態,並且要確認同步器的FIFO隊列爲空,或者隊列不爲空可是本身是隊列中頭結點指向的下一個節點。
這個區別很重要,由於線程在阻塞和非阻塞之間切換時須要比較長的時間,若是恰好線程A釋放了資源,A會去喚醒下一個排着隊的Node節點,當這個喚醒操做還沒完成的時候,這時又來了一個線程B,線程B發現當前沒人持有這個資源,因而本身就迅速拿到了這個資源,充分利用了線程A去喚醒B的這一段時間,這就是公平鎖和非公平鎖之間的差別,這裏也體現了非公平鎖性能較高的地方
ReentrantReadWriteLock
ReentrantReadWriteLock 是 Lock 的另外一種實現方式,咱們已經知道了 ReentrantLock 是一個排他鎖,同一時間只容許一個線程訪問,而 ReentrantReadWriteLock 容許多個讀線程同時訪問,但不容許寫線程和讀線程、寫線程和寫線程同時訪問。相對於排他鎖,提升了併發性。在實際應用中,大部分狀況下對共享數據(如緩存)的訪問都是讀操做遠多於寫操做,這時ReentrantReadWriteLock可以提供比排他鎖更好的併發性和吞吐量。
- ReentrantReadWriteLock支持鎖的降級。
- 讀鎖不支持Condition,會拋出UnsupportedOperationException異常,寫鎖支持Condition
鎖降級/升級
同一個線程中,在沒有釋放讀鎖的狀況下,就去申請寫鎖,這屬於鎖升級,ReentrantReadWriteLock是不支持的
同一個線程中,在沒有釋放寫鎖的狀況下,就去申請讀鎖,這屬於鎖降級,ReentrantReadWriteLock是支持的
鎖降級中讀鎖獲取的意義:
主要是爲了保證數據的可見性,若是當前線程不獲取讀鎖而直接釋放寫鎖,假設此刻另外一個線程(T)獲取了寫鎖並修改了數據,那麼當前線程是沒法感知線程 T 的數據更新。若是當前線程獲取讀鎖,即遵循鎖降級的步驟,則線程T將會被阻塞,直到當前線程使用數據並釋放讀鎖以後,線程 T 才能獲取寫鎖進行數據更新。
CountDownLatch(共享鎖)
CountDownLatch是一個計數器閉鎖,經過它能夠完成相似於阻塞當前線程的功能,即:一個線程或多個線程一直等待,直到其餘線程執行的操做完成。CountDownLatch用一個給定的計數器來初始化,該計數器的操做是原子操做,即同時只能有一個線程去操做該計數器。調用該類 await 方法的線程會一直處於阻塞狀態,直到其餘線程調用countDown方法使當前計數器的值變爲零,每次調用countDown計數器的值減1。當計數器值減至零時,全部因調用await()方法而處於等待狀態的線程就會繼續往下執行。這種現象只會出現一次,由於計數器不能被重置,若是業務上須要一個能夠重置計數次數的版本,能夠考慮使用CycliBarrier。
CountDownLatch主要有兩個方法:countDown() 和 await()。countDown() 方法用於使計數器減一,其通常是執行任務的線程調用,await()方法則使調用該方法的線程處於等待狀態,其通常是主線程調用。這裏須要注意的是,countDown()方法並無規定一個線程只能調用一次,當同一個線程調用屢次countDown()方法時,每次都會使計數器減一;另外,await()方法也並無規定只能有一個線程執行該方法,若是多個線程同時執行await()方法,那麼這幾個線程都將處於等待狀態,而且以共享模式享有同一個鎖。
實現原理
CountDownLatch 是基於 AbstractQueuedSynchronizer 實現的,在AbstractQueuedSynchronizer 中維護了一個 volatile 類型的整數 state,volatile 能夠保證多線程環境下該變量的修改對每一個線程均可見,而且因爲該屬性爲整型,於是對該變量的修改也是原子的。建立一個 CountDownLatch 對象時,所傳入的整數 n 就會賦值給 state 屬性,當 countDown() 方法調用時,該線程就會嘗試對 state 減一,而調用await() 方法時,當前線程就會判斷 state 屬性是否爲 0,若是爲 0,則繼續往下執行,若是不爲0,則使當前線程進入等待狀態,直到某個線程將 state 屬性置爲 0,其就會喚醒在await()方法中等待的線程。
CyclicBarrier
CyclicBarrier 也是一個同步輔助類,它容許一組線程相互等待,直到到達某個公共屏障點(common barrier point)。經過它能夠完成多個線程之間相互等待,只有當每一個線程都準備就緒後,才能各自繼續往下執行後面的操做。
CountDownLatch主要是實現了1個或N個線程須要等待其餘線程完成某項操做以後才能繼續往下執行操做,描述的是1個線程或N個線程等待其餘線程的關係。CyclicBarrier主要是實現了多個線程之間相互等待,直到全部的線程都知足了條件以後各自才能繼續執行後續的操做,描述的多個線程內部相互等待的關係。
CountDownLatch是一次性的,而CyclicBarrier則能夠被重置而重複使用。
Semaphore
Semaphore(信號量)是用來控制同時訪問特定資源的線程數量,它經過協調各個線程,以保證合理的使用公共資源。好比控制用戶的訪問量,同一時刻只容許1000個用戶同時使用系統,若是超過1000個併發,則須要等待。
Semaphore與CountDownLatch類似,不一樣的地方在於Semaphore的值被獲取到後是能夠釋放的,並不像CountDownLatch那樣一直減到底。它也被更多地用來限制流量,相似閥門的 功能。若是限定某些資源最多有N個線程能夠訪問,那麼超過N個主不容許再有線程來訪問,同時當現有線程結束後,就會釋放,而後容許新的線程進來。有點相似於鎖的lock與 unlock過程。相對來講他也有兩個主要的方法:
用於獲取權限的acquire(),其底層實現與CountDownLatch.countdown()相似;
用於釋放權限的release(),其底層實現與acquire()是一個互逆的過程。
參考:https://blog.csdn.net/qq_41573234/article/details/99702344