1. 程式人生 > 實用技巧 >併發程式設計--鎖--鎖的理解及分類

併發程式設計--鎖--鎖的理解及分類

談談你對鎖的理解?

在併發程式設計中有兩個重要的概念:執行緒

多執行緒是一把雙刃劍,它在提高程式效能的同時,也帶來了編碼的複雜性。

鎖的出現就是為了保障多執行緒在同時操作一組資源時的資料一致性,當我們給資源加上鎖之後,只有擁有此鎖的執行緒才能操作此資源,而其他執行緒只能排隊等待使用此鎖。

你知道哪幾種鎖?分別有什麼特點?

需要首先指出的是,這些多種多樣的分類,是評價一個事物的多種標準,比如評價一個城市,標準有人口多少、經濟發達與否、城市面積大小等。而一個城市可能同時佔據多個標準,以北京而言,人口多,經濟發達,同時城市面積還很大。同理,對於 Java 中的鎖而言,一把鎖也有可能同時佔有多個標準,符合多種分類

,比如 ReentrantLock 既是可中斷鎖,又是可重入鎖。

根據分類標準我們把鎖分為以下 7 大類別,分別是:

(1) 悲觀鎖/樂觀鎖;

(2)公平鎖/非公平鎖;

(3) 共享鎖/獨佔鎖;

(4) 可重入鎖/非可重入鎖;

(5)自旋鎖/非自旋鎖;

(6)偏向鎖/輕量級鎖/重量級鎖;

(7) 可中斷鎖/不可中斷鎖。

樂觀鎖/悲觀鎖

悲觀鎖和樂觀鎖並不是某個具體的“鎖”而是一種併發程式設計的基本概念,是根據看待併發同步的角度。樂觀鎖和悲觀鎖最早出現在資料庫的設計當中,後來逐漸被 Java 的併發包所引入。

悲觀鎖

悲觀鎖認為對於同一個資料的併發操作一定是會發生修改的,採取加鎖的形式,悲觀地認為,不加鎖的併發操作一定會出問題。傳統的關係型資料庫裡邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。Java中Synchronized和ReentrantLock等獨佔鎖就是悲觀鎖思想實現的。

樂觀鎖

樂觀鎖正好和悲觀鎖相反,它獲取資料的時候,並不擔心資料被修改,每次獲取資料的時候也不會加鎖,只是在更新資料的時候,通過判斷現有的資料是否和原資料一致來判斷資料是否被其他執行緒操作,如果沒被其他執行緒修改則進行資料更新,如果被其他執行緒修改則不進行資料更新。Lock是樂觀鎖的典型實現案例。

補充:更詳細的介紹請檢視我的另一篇博文 --理解悲觀鎖和樂觀鎖及其實現方式

公平鎖/非公平鎖

根據執行緒獲取鎖的搶佔機制,鎖又可以分為公平鎖和非公平鎖。

公平鎖:是指多個執行緒按照申請鎖的順序來獲取鎖

非公平鎖:是指多個執行緒獲取鎖的順序並不是按照申請鎖的順序,有可能後申請的執行緒比先申請的執行緒優先獲取鎖(允許“插隊”的情況存在)。

舉例說明:

ReentrantLock ,可通過建構函式設定一個 boolean 型別的值,來決定選擇公平鎖和非公平鎖的實現。

公平鎖:new ReentrantLock(true)

非公平鎖:new ReentrantLock(false)

而公平鎖由於有掛起和恢復所以存在一定的開銷,因此效能不如非公平鎖,所以建構函式不傳任何引數的時候,預設提供的是非公平鎖。

獨佔鎖/共享鎖

根據鎖能否被多個執行緒持有,可以把鎖分為獨佔鎖和共享鎖。

獨佔鎖:是指任何時候都只能有一個執行緒能執行資源操作(只能被單執行緒持有的鎖)。比如 synchronized 就是獨佔鎖。

共享鎖:是指可以同時被多個執行緒讀取,但只能被一個執行緒修改。

我們的讀寫鎖,就最好地詮釋了共享鎖和獨佔鎖的理念。讀寫鎖中的讀鎖,是共享鎖,而寫鎖是獨佔鎖。讀鎖可以被同時讀,可以同時被多個執行緒持有,而寫鎖最多隻能同時被一個執行緒持有。

可重入鎖/非可重入鎖

可重入鎖也叫遞迴鎖,指的是執行緒當前已經持有這把鎖了,能在不釋放這把鎖的情況下,再次獲取這把鎖(同一個執行緒,如果外面的函式擁有此鎖之後,內層的函式也可以繼續獲取該鎖)。同理,不可重入鎖指的是雖然執行緒當前持有了這把鎖,但是如果想再次獲取這把鎖,也必須要先釋放鎖後才能再次嘗試獲取。在 Java 語言中 ReentrantLock 和 synchronized 都是可重入鎖,最典型的就是 ReentrantLock 了,正如它的名字一樣,reentrant 的意思就是可重入,它也是 Lock 介面最主要的一個實現類。

下面我們用 synchronized 來演示一下什麼是可重入鎖,程式碼如下:

/**
 * @author 佛大java程式設計師
 * @since 1.0.0
 */
public class LockExample {
    public static void main(String[] args) {
        //可重入鎖A
        reentrantA();
    }

    /**
     * 可重入鎖A方法
     */
    private synchronized static void  reentrantA(){
        System.out.println(Thread.currentThread().getName() + ":執行 reentrantA");
        reentrantB();
    }

    /**
     * 可重入鎖B方法
     */
    private synchronized static void reentrantB(){
        System.out.println(Thread.currentThread().getName() + ":執行 reentrantB");
    }
}

執行結果:

從結果可以看出reentrantA方法和reentrantB方法的執行執行緒都是“main”,我們呼叫了reentrantA方法,它的方法中嵌套了reentrantB,如果 synchronized 是不可重入的話,那麼執行緒會被一直堵塞。

可重入鎖的實現原理,是在鎖內部儲存了一個執行緒標識,用於判斷當前的鎖屬於哪個執行緒,並且鎖的內部維護了一個計數器,當鎖空閒時此計數器的值為0,當被執行緒佔用和重入時分別加1,當鎖被釋放時計數器減1,直到減到 0 時表示此鎖為空閒狀態。

自旋鎖/非自旋鎖

自旋鎖的理念是如果執行緒現在拿不到鎖,並不直接陷入阻塞或者釋放 CPU 資源,而是採用迴圈的方式去嘗試獲取鎖,這個迴圈過程被形象地比喻為“自旋”,就像是執行緒在“自我旋轉”,這樣的好處是減少執行緒上下文切換的消耗,缺點是迴圈會消耗 CPU。相反,非自旋鎖的理念就是沒有自旋的過程,如果拿不到鎖就直接放棄,或者進行其他的處理邏輯,例如去排隊、陷入阻塞等。

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

這三種鎖特指 synchronized 鎖的狀態,通過在物件頭中的 mark word 來表明鎖的狀態。

偏向鎖:它會偏向於第一個獲取鎖的執行緒,如果一把鎖都不存在競爭,那麼其實就沒必要上鎖,只需要打個標記就行了,這就是偏向鎖的思想。一個物件被初始化後,還沒有任何執行緒來獲取它的鎖時,那麼它就是可偏向的,當有第一個執行緒來訪問它並嘗試獲取鎖的時候,它就將這個執行緒記錄下來,以後如果嘗試獲取鎖的執行緒正是偏向鎖的擁有者,就可以直接獲得鎖,開銷很小,效能最好。如果在執行過程中,遇到了其他執行緒搶佔鎖,則持有偏向鎖的執行緒會被掛起,JVM會消除它身上的偏向鎖,將鎖恢復到標準的輕量級鎖。(簡潔版:偏向鎖,它會標記第一個獲取鎖的執行緒,以後如果嘗試獲取鎖的執行緒正是偏向鎖的擁有者,就可以直接獲得鎖,這樣開銷很小,效能最好)

輕量級鎖:JVM 開發者發現在很多情況下,synchronized 中的程式碼是被多個執行緒交替執行的,而不是同時執行的,也就是說並不存在實際的競爭,或者是隻有短時間的鎖競爭,用 CAS 就可以解決,這種情況下,用完全互斥的重量級鎖是沒必要的。輕量級鎖是指當鎖原來是偏向鎖的時候,被另一個執行緒訪問,說明存在競爭,那麼偏向鎖就會升級為輕量級鎖,執行緒會通過自旋的形式嘗試獲取鎖,而不會陷入阻塞。

重量級鎖:重量級鎖是互斥鎖,它是利用作業系統的同步機制實現的,所以開銷相對比較大。當多個執行緒直接有實際競爭,且鎖競爭時間長的時候,輕量級鎖不能滿足需求,鎖就會膨脹為重量級鎖。重量級鎖會讓其他申請卻拿不到鎖的執行緒進入阻塞狀態。

鎖升級的路徑:無鎖→偏向鎖→輕量級鎖→重量級鎖。

綜上所述,偏向鎖效能最好,可以避免執行 CAS 操作。而輕量級鎖利用自旋和 CAS 避免了重量級鎖帶來的執行緒阻塞和喚醒,效能中等。重量級鎖則會把獲取不到鎖的執行緒阻塞,效能最差。

可中斷鎖/不可中斷鎖

在 Java 中,synchronized 關鍵字修飾的鎖代表的是不可中斷鎖,一旦執行緒申請了鎖,就沒有回頭路了,只能等到拿到鎖以後才能進行其他的邏輯處理。而我們的 ReentrantLock 是一種典型的可中斷鎖,例如使用 lockInterruptibly 方法在獲取鎖的過程中,突然不想獲取了,那麼也可以在中斷之後去做其他的事情,不需要一直傻等到獲取到鎖才離開。

常見面試題

(1) 談談你對鎖的理解?

(2) 談談你對樂觀鎖悲觀鎖的理解?

(3) 為什麼非公平鎖吞吐量大於公平鎖?

答:比如 A 佔用鎖的時候,B 請求獲取鎖,發現被 A 佔用之後,堵塞等待被喚醒,這個時候 C 同時來獲取 A 佔用的鎖,如果是公平鎖 C 後來者發現不可用之後一定排在 B 之後等待被喚醒,而非公平鎖則可以讓 C 先用,在 B 被喚醒之前 C 已經使用完成,從而節省了 C 等待和喚醒之間的效能消耗,這就是非公平鎖比公平鎖吞吐量大的原因。

(4)以下說法錯誤的是?

A:獨佔鎖是指任何時候都只有一個執行緒能執行資源操作

B:共享鎖指定是可以同時被多個執行緒讀取和修改

C:公平鎖是指多個執行緒按照申請鎖的順序來獲取鎖

D:非公平鎖是指多個執行緒獲取鎖的順序並不是按照申請鎖的順序,有可能後申請的執行緒比先申請的執行緒優先獲取鎖

題目解析:答案是B,共享鎖指定是可以同時被多個執行緒讀取,但只能被一個執行緒修改。

(5)你知道哪幾種鎖?分別有什麼特點?

參考/好文

拉鉤課程

-- java面試真題及原始碼 --談談你對鎖的理解

--java併發程式設計 --你知道哪幾種鎖?分別有什麼特點?