JAVA鎖詳解——(CAS、PCC、AQS、CLH、Synchronized、Lock、公平/非公平鎖、鎖粗化/消除、分段鎖)

鎖的常縮寫用名詞簡介

CAS(Compare And Swap):

樂觀鎖 併發策略先進行對數據的操作,如果沒有發現其它線程也操作了數據,那麼就認爲這個操作是成功的。如果發生了其它線程也操作了數據,那麼一般採取不斷重試的手段,直到成功或最大重試次數爲止,這種樂觀鎖的策略,不需要把線程阻塞,屬於 非阻塞同步 的一種手段。

PCC(Pessimistic Concurrency Control):

悲觀鎖 在整個數據處理過程中,將數據處於鎖定狀態。禁止其他線程、事務進行修改。典型如數據庫中的行鎖,Java中的synchronized。

AQS(AbstractQueuedSynchronizer):

抽象隊列同步器。AQS就是基於 CLH 隊列,用一個state標記位,線程通過CAS去改變狀態符,成功則獲取鎖成功,失敗則進入等待隊列,等待被喚醒。支持 獨佔共享 兩種方式。
AQS裏面的CLH隊列是CLH同步鎖的一種變形。其主要從兩方面進行了改造:節點的結構節點等待機制

  • 節點結構上引入了頭結點和尾節點,他們分別指向隊列的頭和尾,嘗試獲取鎖、入隊列、釋放鎖等實現都與頭尾節點相關,並且每個節點都引入前驅節點和後後續節點的引用(雙向鏈表)
  • 等待機制由原來的自旋改成 自旋 + 阻塞 + 喚醒

ReentrantLock就是基於 AQS 實現的

CLH(Craig,Landin and Hagersten)發明人名字:
  1. 當線程獲取鎖失敗後入隊尾,指針指向自己的前一個節點
  2. 線程自旋判斷前驅節點爲頭節點則嘗試獲取鎖,成功則將自己設置爲頭結點。失敗則判斷前驅節點waitStatus是否爲-1。如果是就進入阻塞狀態等待被喚醒,否則修改waitStatus爲-1等待下一次自旋。(如前驅節點不是頭結點則直接進入判斷waitStatus是否爲-1的流程)
  3. 釋放鎖的時候判斷自身的waitStatus如果 != 0 則說明後繼節點正在阻塞,等待被喚醒,所以喚醒後繼線程(有效的避免了 驚羣效應 )。

Synchronized詳解

synchronized是什麼

synchronized是Java提供的一個併發控制的關鍵字JVM層面),作用於 對象 上。主要有三種用法:

  • 非靜態方法:鎖作用於訪問的對象上
  • 靜態方法:鎖作用於方法所在的類對象class
  • 代碼塊:鎖作用於括號中指定的對象
synchronized的歷史

JDK1.6以前:synchronized 那時還屬於重量級鎖。
JDK1.6及以後:synchronized 引入鎖升級策略(只能升級不能降級),依次爲 無鎖偏向鎖輕量級鎖重量級鎖 。(因爲大多數時間都不會發生多個線程同時競爭鎖的情況,每次線程都加鎖解鎖,每次這麼搞都要操作系統在用戶態和內核態之間來回切,太耗性能了。)

偏向鎖、輕量級鎖、重量級鎖優缺點對比
      鎖       優點 缺點 使用場景
偏向鎖 加鎖解鎖不需要額外的消耗,和執行非同步方法相比僅存在納秒級的差距 如果線程間存在鎖競爭,會帶來額外的鎖撤銷的消耗 使用於基本只有一個線程訪問同步塊的場景
輕量級鎖 多個線程競爭不會阻塞,提高程序響應速度 如果始終得不到鎖,線程自旋會消耗CPU 追求響應時間,同步代碼塊執行耗時短
重量級鎖 多個線程競爭不自旋,節省CPU消耗 線程阻塞,需要喚醒,相對耗時較長 追求吞吐量,同步代碼塊執行耗時較長
  • 偏向鎖
    • 對象頭 信息 Mark Word 裏存儲鎖偏向的 線程ID ,同一個線程會自動獲取鎖。
    • 偏向鎖只有遇到其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程纔會釋放鎖,線程不會主動釋放偏向鎖。
    • 如果確定同步方法會被高併發的訪問,建議通過-XX:-UseBiasedLocking 參數關閉偏向鎖
  • 輕量級鎖
    • 當鎖是偏向鎖的時候,被另外的線程所訪問,偏向鎖就會升級爲輕量級鎖,其他線程會通過 自旋 的形式嘗試獲取鎖,不會阻塞,從而提高性能。
    • 若當前只有一個等待線程,則該線程通過自旋進行等待。但是當自旋超過一定的次數,或者一個線程在持有鎖,一個在自旋,又有第三個來訪時,輕量級鎖升級爲重量級鎖。
  • 重量級鎖
    • 此時Mark Word中存儲的是指向重量級鎖的指針,此時等待鎖的線程都會進入阻塞狀態等待被喚醒。通過對象內部的監視器(Monitor)實現。
    • Monitor對象重要屬性說明(並非所有的屬性):
      • _owner:指向持有ObjectMonitor對象的線程,當線程釋放monitor時,_owner又恢復爲NULL。
      • _WaitSet:存放處於wait狀態的線程隊列,因爲調用wait方法而被阻塞的線程會被放在該隊列中
      • _EntryList:存放處於等待鎖block狀態的線程隊列
      • _recursions:鎖的重入次數
      • _count:用來記錄該線程獲取鎖的次數(當_count = 0、_recursions = 0 則釋放鎖,喚醒EntryList中的線程)

既然聊到了這裏咱在瞭解下對象頭。
對象頭主要包括兩部分數據:Mark Word(標記字段)、Klass Pointer(類型指針)
Mark Word:默認存儲對象的HashCode、分代年齡、GC次數和鎖標誌位信息。
Klass Point:對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。
對象頭信息

鎖升級流程

Synchronized鎖升級流程

Lock對比Synchronized

對比項 Synchronized Lock
存在層次 JAVA關鍵字,JVM層面 是一個接口
鎖釋放 1. 獲取鎖的線程執行完同步代碼,釋放鎖
2. 程執行發生異常,jvm會讓線程釋放鎖
調用unlock()方法釋放鎖
鎖的獲取 假設A線程獲得鎖,B線程等待。如果A線程阻塞,B線程會一直等待 Lock有多種獲取鎖的方式,如lock、tryLock
鎖狀態 無法判斷 可判斷
可避免死鎖:tryLock(long time, TimeUnit unit)
鎖類型 可重入
非公平
不可中斷
可重入
可公平/非公平
可中斷:lockInterruptibly()

公平鎖

公平鎖是指多個線程按照申請鎖的順序來獲取鎖,線程直接進入隊列中排隊,隊列中的第一個線程才能獲得鎖。公平鎖的優點是等待鎖的線程不會餓死。缺點是整體吞吐效率相對非公平鎖要低,等待隊列中除第一個線程以外的所有線程都會阻塞,CPU喚醒阻塞線程的開銷比非公平鎖大。

非公平鎖

非公平鎖是多個線程加鎖時直接嘗試獲取鎖,獲取不到纔會到等待隊列的隊尾等待。但如果此時鎖剛好可用,那麼這個線程可以無需阻塞直接獲取到鎖,所以非公平鎖有可能出現後申請鎖的線程先獲取鎖的場景。非公平鎖的優點是可以減少喚起線程的開銷,整體的吞吐效率高,因爲線程有機率不阻塞直接獲得鎖,CPU不必喚醒所有線程。缺點是處於等待隊列中的線程可能會餓死,或者等很久纔會獲得鎖。

鎖粗化

原則上,我們在編寫同步塊的時候,同步塊的範圍應當儘量小——只在共享數據的實際作用域中才進行同步,這樣是爲了使得需要同步的操作數量儘可能小,如果存在鎖競爭,那等待鎖的線程也能儘快的拿到鎖。
大部分情況下,這種原則是正確的,但是如果一系列的連續操作都需要對同一個對象進行加鎖和解鎖,甚至加鎖操作時出現在循環體中,那即使沒有線程競爭,頻繁地進行互斥同步操作也會導致不需要的性能損耗。這時JIT編譯器就會把鎖同步的範圍擴展(粗化)。
例如我們有如下代碼 ↓

for(int i=0;i<size;i++){
    synchronized(lock){
    ...
    }
}

鎖粗化後的代碼如下 ↓

synchronized(lock){
    for(int i=0;i<size;i++){
    ...
    }
}

鎖消除

鎖消除是發生在編譯器級別的一種鎖優化方式。將檢測到不可能存在共享數據競爭的鎖進行削除。
例如我們有如下代碼 ↓

public void method() {
        Object object = new Object();
        synchronized (object) {
            System.out.println("Hello world");
        }
    }

object 本身就是局部變量,方法的的局部變量是線程獨立的,併發的場景每個線程都有各自的object對象,這個時候的鎖就無意義的。

分段鎖

分段鎖其實是一種鎖的設計,並不是具體的一種鎖。
典型如 ConcurrentHashMap ,其併發的實現就是通過分段鎖的形式來實現高效的併發操作。ConcurrentHashMap中的分段鎖稱爲 Segment ,它即類似於HashMap的結構,即 內部擁有一個Entry數組,數組中的每個元素又是一個鏈表;同時又是一個ReentrantLock(Segment繼承了ReentrantLock)
當需要put元素的時候,並不是對整個Hashmap進行加鎖,而是先通過 HashCode 來知道他要放在那一個分段中,然後 對這個分段進行加鎖 ,所以當多線程put的時候,只要不是放在一個分段中,就實現了真正的並行的插入。
但是,在 統計size 的時候,可就是獲取Hashmap全局信息的時候,就需要 獲取所有的分段鎖 才能統計。分段鎖的設計目的是細化鎖的粒度,當操作不需要更新整個數組的時候,就僅僅針對數組中的一項進行加鎖操作。

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