Java鎖:悲觀/樂觀/阻塞/自旋/公平鎖/閉鎖,鎖消除CAS及synchronized的三種鎖級別

JAVA LOCK 大全

[TOC]

一、廣義分類:樂觀鎖/悲觀鎖

1.1 樂觀鎖的實現CAS (Compare and Swap)

樂觀鎖適合低併發的情況,在高併發的情況下由於自旋,性能甚至可能悲觀鎖更差。

CAS是一種算法,CAS(V,E,N),V:要更新的變量 E:預期值 N:新值。

  • 如果多個線程進行CAS操作,只有一個會成功,其餘的會失敗(允許再次嘗試)。
  • CAS是樂觀鎖的一種帶自選的實現算法(對象和類的關係)。
  • 操作系統保證CAS的執行是CPU原子指令。

1.2 sun.misc.Unsafe

Java中CAS操作的執行依賴於sun.misc.Unsafe類的方法,Unsafe中的方法都是native的。

  • (Unsafe類,非線程安全,擁有類似C的指針操作,Java官方不建議直接使用的Unsafe類)

 

    //Usafe的幾個CAS方法
    public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
    public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

1.3 java.util.concurrent.atomic

併發包中的原子操作類(java.util.concurrent.atomic),在該包中提供了許多基於CAS實現的原子操作類。
這些方法都是基於調用Unsafe類實現的。

1.4 CAS的ABA問題 AtomicStampedReference&AtomicMarkableReference

  1. ABA問題是反覆讀寫問題,在多個線程並行時,一個線程把1改成2,另一個線程又把2改成1的情況。

  2. CSA的ABA問題可以使用 AtomicStampedReference&AtomicMarkableReference兩個類來避免。

  3. AtomicStampedReference 是一個帶有時間戳的對象引用。在每次修改後不僅會設置新值,還會記錄更改的時間。當該類設置對象時必須同時滿足時間戳和期望值才能寫入成功。避免了反覆讀寫問題。

  4. AtomicMarkableReference 是使用了一個bool值來標記修改,原理與AtomicStampedReference類似,不能避免ABA問題,可以減少發生概率。

1.5 悲觀鎖(讀寫鎖是悲觀鎖的兩種實現)

1.5.1 ReentrantReadWriteLock 可重入讀寫鎖

ReentrantReadWriteLock的構造函數接受一個bool fair 用來指定是否是fair公平鎖。默認是unfair.

 

    private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    private final Lock r = rwl.readLock();
    private final Lock w = rwl.writeLock();

使用讀寫鎖的時候,主動加鎖(lock),一般在finally中釋放鎖(unlock)。

1.5.2 Synchronized

經過不斷的優化(詳見 三、JAVA Synchronized 鎖的三種級別),在低併發情況下性能很好。

二、Java鎖的兩種實現:ReentrantLock 與 Synchronized

可重入鎖ReentrantLock 類實現了 Lock ,它擁有與 synchronized 相同的併發性和內存語義。
添加了類似鎖投票、定時鎖等候和可中斷鎖等候的一些特性。

此外,它還提供了在激烈爭用情況下更佳的性能
(當許多線程都想訪問共享資源時,JVM 可以花更少的時候來調度線程,把更多時間用在執行線程上)

它有一個與鎖相關的獲取計數器,如果擁有鎖的某個線程再次得到鎖,那麼獲取計數器就加1,然後鎖需要被釋放兩次才能獲得真正釋放。
這模仿了 synchronized 的語義:如果線程進入由線程已經擁有的監控器保護的 synchronized 塊,就允許線程繼續進行,當線程退出第二個(或者後續) synchronized 塊的時候,不釋放鎖,只有線程退出它進入的監控器保護的第一個 synchronized 塊時,才釋放鎖。

IBM技術論壇中介紹 synchronized 和ReentrantLock的文章。(Jdk5)
文章的主要論述:synchronized 的功能集是 ReentrantLock 的子集。
ReentrantLock 多了:時間鎖等候、可中斷鎖等候、無塊結構鎖、多個條件變量或者鎖投票等特性。
所以 ReentrantLock 從功能上來說完全可以取代 synchronized。但是實際使用中不用這麼絕對。
synchronized只有一個好處,使用方便簡單,不用主動釋放鎖。

文章寫於jdk5時期,jdk6給synchronized引入了偏向鎖等優化。性能差距越來越小。
所以除非用到ReentrantLock的獨有特性。其他情況下也可以繼續使用Synchronized.

三、synchronized 性能優化:Synchronized的三種級別

無鎖、偏向、輕量、重量幾種級別的轉換圖如下:

 

sync鎖級別轉化.png

3.1 Biased Locking 偏向鎖(輕量級鎖的多線程優化技術jdk6引入)

是Java6引入的一項針對輕量級鎖的多線程優化技術。

  • 偏向鎖,顧名思義,它會偏向於第一個訪問鎖的線程,如果在運行過程中,同步鎖只有一個線程訪問,不存在多線程爭用的情況,則線程是不需要觸發同步的,這種情況下,就會給線程加一個偏向鎖。
  • 如果在運行過程中,遇到了其他線程搶佔鎖,則持有偏向鎖的線程會被掛起,JVM會消除它身上的偏向鎖,將鎖恢復到標準的輕量級鎖。
  • 它通過消除資源無競爭情況下的同步原語,進一步提高了程序的運行性能。但當程序有大量競爭情況,應該關閉該特性。

 

//開啓偏向鎖
-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
//關閉偏向鎖
-XX:-UseBiasedLocking

3.2 輕量級鎖

由偏向鎖升級,當第二個線程加入鎖競爭的時候,偏向鎖就升級爲輕量級鎖。
加鎖過程:

  1. markWord鎖標誌位爲無鎖狀態01時,在當前線程的棧幀中創建一個Lock Record 用來拷貝目前對象的markWord。
  2. 拷貝成功後,JVM使用CAS嘗試將對象的markWord指向Lock Record。如果成功執行3,失敗執行4。
  3. 成功更新了markWord的指針後,該線程就有了該對象的鎖,會將markWord中的鎖標誌爲設爲00:輕量鎖。
  4. 更新失敗了,則先檢查對象的markWord是否指向該線程的棧幀(Stack裏的)。如果是則其實已經獲取鎖了,如果不是則說明多線程競爭,則鎖膨脹爲重量級鎖定10

markWord存儲內容(最後2bit是鎖狀態在無鎖和偏向鎖兩種狀態下,2bit前的1bit標識是否偏向)

狀態 鎖標誌位(2bit) markWord存儲內容
未鎖定 01 對象哈希碼、對象分代年齡
輕量級鎖定 00 指向鎖記錄的指針
膨脹(重量級鎖定) 10 執行重量級鎖定的指針
GC標記 11 空(不需要記錄信息)
可偏向 01 偏向線程ID、偏向時間戳、對象分代年齡

具體的存儲內容如下:

 

markWord_lock.jpg

3.3 重量級鎖

重量級鎖發生在輕量鎖釋放鎖的期間,之前在獲取鎖的時候它拷貝了鎖對象頭的markWord,在釋放鎖的時候如果它發現在它持有鎖的期間有其他線程來嘗試獲取鎖了,並且該線程對markWord做了修改,兩者比對發現不一致,則切換到重量鎖。

四、其他鎖:阻塞BlockingLock/自旋鎖SpinLock/公平fairLock /unfairLock/閉鎖Latch

4.1 阻塞鎖 Blocking lock

阻塞鎖會有線程切換的代價,但是阻塞鎖阻塞後不佔用CPU。
阻塞鎖一般是悲觀鎖。

4.2 自旋鎖 Spin lock

  • 自旋鎖原理非常簡單,如果持有鎖的線程能在很短時間內釋放鎖資源,那麼那些等待競爭鎖的線程就不需要做內核態和用戶態之間的切換進入阻塞掛起狀態,它們只需要等一等(自旋),等持有鎖的線程釋放鎖後即可立即獲取鎖,這樣就避免用戶線程和內核的切換的消耗。
  • 性能原因,一般JVM會限制自旋等待時間。
    自旋鎖一般是樂觀鎖。

4.2.1 自旋鎖優缺點

  • 優點:在鎖競爭不激烈的情況下,佔用鎖的時間非常短的代碼來說,自旋操作(cpu空轉)的消耗小於線程阻塞掛起的消耗。
  • 缺點:如果鎖競爭激烈,或者持有鎖的線程需要長時間佔用鎖執行同步塊,就不適合自旋鎖,這是CPU空轉的消耗大於線程阻塞的消耗。

Java線程切換的代價:
Java的線程是映射到操作系統線程上的,如果要阻塞或喚醒一個線程就需要操作系統介入,需要在用戶態與和心態之間切換。

  • 內核態: CPU可以訪問內存所有數據, 包括外圍設備, 例如硬盤, 網卡. CPU也可以將自己從一個程序切換到另一個程序
  • 用戶態: 只能受限的訪問內存, 且不允許訪問外圍設備. 佔用CPU的能力被剝奪, CPU資源可以被其他程序獲取

jdk1.6默認開啓自旋鎖,從JVM的層面對顯示鎖(都是悲觀鎖)做優化,"智能"的決定自旋次數。
而樂觀鎖通過CAS實現,非阻塞,失敗後繼續獲取還是放棄的實現不確定,只能程序員從代碼層面對樂觀鎖做自旋(我稱之爲自旋樂觀鎖)。

4.3 fair/unfair

公平鎖,非公平鎖。
公平鎖維護了一個隊列。要獲取鎖的線程來了都排隊。後續的線程按照隊列順序來獲取鎖。
非公平鎖沒有維護隊列的開銷,沒有上下文切換的開銷,可能導致不公平,但是性能比fair好很多。

ReentrantLock的帶參構造函數ReentrantLock(boolean fair)可以指定實現公平還是非公平鎖。默認是非公平鎖。

4.4 閉鎖 Latch

閉鎖(Latch)是一種同步工具類,可以延遲線程的進度直到其到達終止狀態。
閉鎖的作用相當於一扇門:在閉鎖到達結束狀態之前,這扇門一直是關閉的,並且沒有任何線程能通過,當到達結束狀態時,這扇門會打開並允許所有的線程通過。

Java中CountDownLatch是一種閉鎖實現,位於concurrent包下。

4.5 鎖消除

鎖消除指的是在JVM即使編譯時,通過運行少下文的掃描,去除不可能存在共享資源競爭的鎖。
通過鎖消除,可以節省毫無意義的鎖請求.

比如在單線程下使用StringBuffer,其中的同步完全沒有必要,這時候JVM可以在運行時基於逃逸分析計數,消除不必要的鎖。

五、如何避免死鎖

死鎖是類似這樣的情況:a,b兩個線程,a持有鎖A 等待鎖B;b持有鎖B等待鎖A。a,b相互等待,誰也執行不下去。
避免死鎖的原則是

  1. 避免持有多個鎖。
  2. 如果確實需要多個鎖,所有代碼都應該按照相同的順序去申請鎖。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章