史上最全 Java 中各類鎖的介紹

更多精彩原創內容請關注:JavaInterview,歡迎 star,支持鼓勵如下做者,萬分感謝。前端

鎖的分類介紹

樂觀鎖與悲觀鎖

鎖的一種宏觀分類是樂觀鎖悲觀鎖。樂觀鎖與悲觀鎖並非特定的指哪一個鎖(Java 中也沒有那個具體鎖的實現名就叫
樂觀鎖或悲觀鎖),而是在併發狀況下兩種不一樣的策略。java

樂觀鎖(Optimistic Lock)就是很樂觀,每次去拿數據的時候都認爲別人不會修改。因此不會上鎖。可是若是想要更新數據,
則會在更新以前檢查在讀取至更新這段時間別人有沒有修改過這個數據。若是修改過,則從新讀取,再次嘗試更新,循環上述
步驟直到更新成功(固然也容許更新失敗的線程放棄更新操做)。git

悲觀鎖(Pessimistic Lock)就是很悲觀,每次去拿數據的時候都認爲別人會修改。因此每次都在拿數據的時候上鎖。
這樣別人拿數據的時候就會被擋住,直到悲觀鎖釋放,想獲取數據的線程再去獲取鎖,而後再獲取數據。github

悲觀鎖阻塞事務,樂觀鎖回滾重試,它們個有優缺點,沒有好壞之分,只有適應場景的不一樣區別。好比:樂觀鎖適合用於寫
比較少的狀況下,即衝突真的不多發生的場景,這樣能夠省去鎖的開銷,加大了系統的整個吞吐量。可是若是常常產生衝突,上層
應用會不斷的進行重試,這樣反而下降了性能,因此這種場景悲觀鎖比較合適。
總結:樂觀鎖適合寫比較少,衝突不多發生的場景;而寫多,衝突多的場景適合使用悲觀鎖算法

樂觀鎖的基礎 --- CAS

在樂觀鎖的實現中,咱們必需要了解的一個概念:CAS。編程

什麼是 CAS 呢? Compare-and-Swap,即比較並替換,或者比較並設置網絡

  • 比較:讀取到一個值 A,在將其更新爲 B 以前,檢查原值是否爲 A(未被其它線程修改過,這裏忽略 ABA 問題)。併發

  • 替換:若是是,更新 A 爲 B,結束。若是不是,則不會更新。函數

上面兩個步驟都是原子操做,能夠理解爲瞬間完成,在 CPU 看來就是一步操做。源碼分析

有了 CAS,就能夠實現一個樂觀鎖:

public class OptimisticLockSample{
    
    public void test(){
        int data = 123; // 共享數據
        
        // 更新數據的線程會進行以下操做
        for (;;) {
            int oldData = data;
            int newData = doSomething(oldData);
            
            // 下面是模擬 CAS 更新操做,嘗試更新 data 的值
            if (data == oldData) { // compare
                data = newData; // swap
                break; // finish
            } else {
                // 什麼都不作,循環重試
            }
        }   
    }
    
    /**
    * 
    * 很明顯,test() 裏面的代碼根本不是原子性的,只是展現了下 CAS 的流程。
    * 由於真正的 CAS 利用了 CPU 指令。
    *  
    * */ 
    

}

在 Java 中也是經過 native 方法實現的 CAS。

public final class Unsafe {
    
    ...
    
    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);  
    
    ...
}

上面寫了一個簡單直觀的樂觀鎖(確切的來講應該是樂觀鎖流程)的實現,它容許多個線程同時讀取(由於根本沒有加鎖操做),若是更新數據的話,
有且僅有一個線程能夠成功更新數據,並致使其它線程須要回滾重試。CAS 利用 CPU 指令,從硬件層面保證了原子性,以達到相似於鎖的效果。

從樂觀鎖的整個流程中能夠看出,並無加鎖解鎖的操做,所以樂觀鎖策略也被稱做爲無鎖編程。換句話說,樂觀鎖其實不是"鎖",
它僅僅是一個循環重試的 CAS 算法而已。

自旋鎖

synchronized 與 Lock interface

Java 中兩種實現加鎖的方式:一種是使用 synchronized 關鍵字,另外一種是使用 Lock 接口的實現類。

在一篇文章中看到一個好的對比,很是形象,synchronized 關鍵字就像是自動擋,能夠知足一切的駕駛需求。
可是若是你想要作更高級的操做,好比玩漂移或者各類高級的騷操做,那麼就須要手動擋,也就是 Lock 接口的實現類。

而 synchronized 在通過 Java 每一個版本的各類優化後,效率也變得很高了。只是使用起來沒有 Lock 接口的實現類那麼方便。

synchronized 鎖升級過程就是其優化的核心:偏向鎖 -> 輕量級鎖 -> 重量級鎖

class Test{
    private static final Object object = new Object(); 
    
    public void test(){
        synchronized(object) {
            // do something        
        }   
    }
    
}

使用 synchronized 關鍵字鎖住某個代碼塊的時候,一開始鎖對象(就是上述代碼中的 object)並非重量級鎖,而是偏向鎖。
偏向鎖的字面意思就是"偏向於第一個獲取它的線程"的鎖。線程執行完同步代碼塊以後,並不會主動釋放偏向鎖。當第二次到達同步
代碼塊時,線程會判斷此時持有鎖的線程是否就是本身(持有鎖的線程 ID 在對象頭裏存儲),若是是則正常往下執行。因爲以前沒有釋放,
這裏就不須要從新加鎖
,若是從頭至尾都是一個線程在使用鎖,很明顯偏向鎖幾乎沒有額外開銷,性能極高。

一旦有第二個線程加入鎖競爭,偏向鎖轉換爲輕量級鎖自旋鎖)。鎖競爭:若是多個線程輪流獲取一個鎖,可是每次獲取的時候
都很順利,沒有發生阻塞,那麼就不存在鎖競爭。只有當某線程獲取鎖的時候,發現鎖已經被佔用,須要等待其釋放,則說明發生了鎖競爭。

在輕量級鎖狀態上繼續鎖競爭,沒有搶到鎖的線程進行自旋操做,即在一個循環中不停判斷是否能夠獲取鎖。獲取鎖的操做,就是經過 CAS 操
做修改對象頭裏的鎖標誌位。先比較當前鎖標誌位是否爲釋放狀態,若是是,將其設置爲鎖定狀態,比較並設置是原子性操做,這個
是 JVM 層面保證的。當前線程就算持有了鎖,而後線程將當前鎖的持有者信息改成本身。

假如咱們獲取到鎖的線程操做時間很長,好比會進行復雜的計算,數據量很大的網絡傳輸等;那麼其它等待鎖的線程就會進入長時間的自旋操做,這個
過程是很是耗資源的。其實這時候至關於只有一個線程在有效地工做,其它的線程什麼都幹不了,在白白地消耗 CPU,這種現象叫作忙等
(busy-waiting)
。因此若是多個線程使用獨佔鎖,可是沒有發生鎖競爭,或者發生了很輕微的鎖競爭,那麼 synchronized 就是輕量
級鎖,容許短期的忙等現象。這是一種擇中的想法,短期的忙等,換取線程在用戶態和內核態之間切換的開銷

顯然,忙等是有限度的(JVM 有一個計數器記錄自旋次數,默認容許循環 10 次,能夠經過虛擬機參數更改)。若是鎖競爭狀況嚴重,
達到某個最大自旋次數的線程,會將輕量級鎖升級爲重量級鎖(依然是經過 CAS 修改鎖標誌位,但不修改持有鎖的線程 ID)。當後續線程嘗試獲取
鎖時,發現被佔用的鎖是重量級鎖,則直接將本身掛起(而不是上面說的忙等,即不會自旋),等待釋放鎖的線程去喚醒。在 JDK1.6 以前, synchronized
直接加劇量級鎖,很明顯如今經過一系列的優化事後,性能明顯獲得了提高。

JVM 中,synchronized 鎖只能按照偏向鎖、輕量級鎖、重量級鎖的順序逐漸升級(也有把這個稱爲鎖膨脹的過程),不容許降級。

可重入鎖(遞歸鎖)

可重入鎖的字面意思是"能夠從新進入的鎖",即容許同一個線程屢次獲取同一把鎖。好比一個遞歸函數裏有加鎖操做,遞歸函數裏這個鎖會阻塞本身麼?
若是不會,那麼這個鎖就叫可重入鎖(由於這個緣由可重入鎖也叫作遞歸鎖)。

Java 中以 Reentrant 開頭命名的鎖都是可重入鎖,並且 JDK 提供的全部現成 Lock 的實現類,包括 synchronized 關鍵字鎖都是可重入的
若是真的須要不可重入鎖,那麼就須要本身去實現了,獲取去網上搜索一下,有不少,本身實現起來也很簡單。

若是不是可重入鎖,在遞歸函數中就會形成死鎖,因此 Java 中的鎖基本都是可重入鎖,不可重入鎖的意義不是很大,我暫時沒有想到什麼場景下會用到;
注意:有想到須要不可重入鎖場景的小夥伴們能夠留言一塊兒探討

下圖展現一下 Lock 的相關實現類:
ilock.png

公平鎖和非公平鎖

若是多個線程申請一把公平鎖,那麼得到鎖的線程釋放鎖的時候,先申請的先獲得,很公平。若是是非公平鎖,後申請的線程可能先得到鎖,是
隨機獲取仍是其它方式,都是根據實現算法而定的。

對 ReentrantLock 類來講,經過構造函數能夠指定該鎖是不是公平鎖,默認是非公平鎖。由於在大多數狀況下,非公平鎖的吞吐量比公平鎖的大,
若是沒有特殊要求,優先考慮使用非公平鎖。

而對於 synchronized 鎖而言,它只能是一種非公平鎖,沒有任何方式使其變成公平鎖。這也是 ReentrantLock 相對於 synchronized 鎖的一個
優勢,更加的靈活。

如下是 ReentrantLock 構造器代碼:

/**
 * Creates an instance of {@code ReentrantLock} with the
 * given fairness policy.
 *
 * @param fair {@code true} if this lock should use a fair ordering policy
 */
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

ReentrantLock 內部實現了 FairSync 和 NonfairSync 兩個內部類來實現公平鎖和非公平鎖。具體源碼分析會在接下來的章節給出,敬請關注
該項目,歡迎 forkstar

可中斷鎖

字面意思是"能夠響應中斷的鎖"。

首先,咱們須要理解的是什麼是中斷。 Java 中並無提供任何能夠直接中斷線程的方法,只提供了中斷機制。那麼何爲中斷機制呢?
線程 A 向線程 B 發出"請你中止運行"的請求,就是調用 Thread.interrupt() 的方法(固然線程 B 自己也能夠給本身發送中斷請求,
即 Thread.currentThread().interrupt()),但線程 B 並不會當即中止運行,而是自行選擇在合適的時間點以本身的方式響應中斷,也能夠
直接忽略此中斷。也就是說,Java 的中斷不能直接終止線程,只是設置了狀態爲響應中斷的狀態,須要被中斷的線程本身決定怎麼處理。這就像
在讀書的時候,老師在晚自習時叫學生本身複習功課,但學生是否複習功課,怎麼複習功課則徹底取決於學生本身。

回到鎖的分析上來,若是線程 A 持有鎖,線程 B 等待持獲取該鎖。因爲線程 A 持有鎖的時間過長,線程 B 不想繼續等了,咱們可讓線程 B 中斷
本身或者在別的線程裏面中斷 B,這種就是 可中段鎖

在 Java 中, synchronized 鎖是不可中斷鎖,而 Lock 的實現類都是 可中斷鎖。從而能夠看出 JDK 本身實現的 Lock 鎖更加的
靈活,這也就是有了 synchronized 鎖後,爲何還要實現那麼些 Lock 的實現類。

Lock 接口的相關定義:

public interface Lock {

    void lock();

    void lockInterruptibly() throws InterruptedException;

    boolean tryLock();
    
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    
    void unlock();

    Condition newCondition();
}

其中 lockInterruptibly 就是獲取可中斷鎖。

共享鎖

字面意思是多個線程能夠共享一個鎖。通常用共享鎖都是在讀數據的時候,好比咱們能夠容許 10 個線程同時讀取一份共享數據,這時候咱們
能夠設置一個有 10 個憑證的共享鎖。

在 Java 中,也有具體的共享鎖實現類,好比 Semaphore。 該類的源碼分析會在後續章節進行分析,敬請關注該項目,歡迎 forkstar

互斥鎖

字面意思是線程之間互相排斥的鎖,也就是代表鎖只能被一個線程擁有。

在 Java 中, ReentrantLock、synchronized 鎖都是互斥鎖。

讀寫鎖

讀寫鎖實際上是一對鎖,一個讀鎖(共享鎖)和一個寫鎖(互斥鎖、排他鎖)。

在 Java 中, ReadWriteLock 接口只規定了兩個方法,一個返回讀鎖,一個返回寫鎖。

public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading
     */
    Lock readLock();

    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing
     */
    Lock writeLock();
}

文章前面講過樂觀鎖策略,全部線程能夠隨時讀,僅在寫以前判斷值有沒有被更改。

讀寫鎖其實作的事情是同樣的,可是策略稍有不一樣。不少狀況下,線程知道本身讀取數據後,是不是爲了更改它。那麼爲什麼不在加鎖的時候直接明確
這一點呢?若是我讀取值是爲了更新它(SQL 的 for update 就是這個意思),那麼加鎖的時候直接加寫鎖,我持有寫鎖的時候,別的線程
不管是讀仍是寫都須要等待;若是讀取數據僅僅是爲了前端展現,那麼加鎖時就明確加一個讀鎖,其它線程若是也要加讀鎖,不須要等待,能夠
直接獲取(讀鎖計數器加 1)。

雖然讀寫鎖感受與樂觀鎖有點像,可是讀寫鎖是悲觀鎖策略。由於讀寫鎖並無在更新前判斷值有沒有被修改過,而是在加鎖前決定
應該用讀鎖仍是寫鎖。樂觀鎖特指無鎖編程。

JDK 內部提供了一個惟一一個 ReadWriteLock 接口實現類是 ReentrantReadWriteLock。經過名字能夠看到該鎖提供了讀寫鎖,而且也是
可重入鎖。

總結

Java 中使用的各類鎖基本都是悲觀鎖,那麼 Java 中有樂觀鎖麼?結果是確定的,那就是 java.util.concurrent.atomic 下面的
原子類都是經過樂觀鎖實現的。以下:

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

經過上述源碼能夠發現,在一個循環裏面不斷 CAS,直到成功爲止。

參數介紹

-XX:-UseBiasedLocking=false 關閉偏向鎖

JDK1.6 

-XX:+UseSpinning 開啓自旋鎖

-XX:PreBlockSpin=10 設置自旋次數 

JDK1.7 以後 去掉此參數,由 JVM 控制
相關文章
相關標籤/搜索