完全掌握Java鎖(圖文解析)

2022-06-14 14:02:21
本篇文章給大家帶來了關於的相關知識,其中主要介紹了關於java鎖的相關問題,包括了獨佔鎖、悲觀鎖、樂觀鎖、共用鎖等等內容,下面一起來看一下,希望對大家有幫助。

推薦學習:《》

樂觀鎖和悲觀鎖

悲觀鎖

悲觀鎖對應於生活中悲觀的人,悲觀的人總是想著事情往壞的方向發展。

舉個生活中的例子,假設廁所只有一個坑位了,悲觀鎖上廁所會第一時間把門反鎖上,這樣其他人上廁所只能在門外等候,這種狀態就是「阻塞」了。

回到程式碼世界中,一個共用資料加了悲觀鎖,那執行緒每次想操作這個資料前都會假設其他執行緒也可能會操作這個資料,所以每次操作前都會上鎖,這樣其他執行緒想操作這個資料拿不到鎖只能阻塞了。

20210606232504-2021-06-06-23-25-04

在 Java 語言中 synchronizedReentrantLock等就是典型的悲觀鎖,還有一些使用了 synchronized 關鍵字的容器類如 HashTable 等也是悲觀鎖的應用。

樂觀鎖

樂觀鎖 對應於生活中樂觀的人,樂觀的人總是想著事情往好的方向發展。

舉個生活中的例子,假設廁所只有一個坑位了,樂觀鎖認為:這荒郊野外的,又沒有什麼人,不會有人搶我坑位的,每次關門上鎖多浪費時間,還是不加鎖好了。你看樂觀鎖就是天生樂觀!

回到程式碼世界中,樂觀鎖運算元據時不會上鎖,在更新的時候會判斷一下在此期間是否有其他執行緒去更新這個資料。

20210606232434-2021-06-06-23-24-35

樂觀鎖可以使用版本號機制CAS演演算法實現。在 Java 語言中 java.util.concurrent.atomic包下的原子類就是使用CAS 樂觀鎖實現的。

兩種鎖的使用場景

悲觀鎖和樂觀鎖沒有孰優孰劣,有其各自適應的場景。

樂觀鎖適用於寫比較少(衝突比較小)的場景,因為不用上鎖、釋放鎖,省去了鎖的開銷,從而提升了吞吐量。

如果是寫多讀少的場景,即衝突比較嚴重,執行緒間競爭激勵,使用樂觀鎖就是導致執行緒不斷進行重試,這樣可能還降低了效能,這種場景下使用悲觀鎖就比較合適。

獨佔鎖和共用鎖

獨佔鎖

獨佔鎖是指鎖一次只能被一個執行緒所持有。如果一個執行緒對資料加上排他鎖後,那麼其他執行緒不能再對該資料加任何型別的鎖。獲得獨佔鎖的執行緒即能讀資料又能修改資料。

20210606232544-2021-06-06-23-25-45

JDK中的synchronizedjava.util.concurrent(JUC)包中Lock的實現類就是獨佔鎖。

共用鎖

共用鎖是指鎖可被多個執行緒所持有。如果一個執行緒對資料加上共用鎖後,那麼其他執行緒只能對資料再加共用鎖,不能加獨佔鎖。獲得共用鎖的執行緒只能讀資料,不能修改資料。

20210606232612-2021-06-06-23-26-13

在 JDK 中 ReentrantReadWriteLock 就是一種共用鎖。

互斥鎖和讀寫鎖

互斥鎖

互斥鎖是獨佔鎖的一種常規實現,是指某一資源同時只允許一個存取者對其進行存取,具有唯一性和排它性。

20210606232634-2021-06-06-23-26-35

互斥鎖一次只能一個執行緒擁有互斥鎖,其他執行緒只有等待。

讀寫鎖

讀寫鎖是共用鎖的一種具體實現。讀寫鎖管理一組鎖,一個是唯讀的鎖,一個是寫鎖。

讀鎖可以在沒有寫鎖的時候被多個執行緒同時持有,而寫鎖是獨佔的。寫鎖的優先順序要高於讀鎖,一個獲得了讀鎖的執行緒必須能看到前一個釋放的寫鎖所更新的內容。

讀寫鎖相比於互斥鎖並行程度更高,每次只有一個寫執行緒,但是同時可以有多個執行緒並行讀。

20210606232658-2021-06-06-23-26-59

在 JDK 中定義了一個讀寫鎖的介面:ReadWriteLock

public interface ReadWriteLock {
    /**
     * 獲取讀鎖
     */
    Lock readLock();

    /**
     * 獲取寫鎖
     */
    Lock writeLock();
}

ReentrantReadWriteLock 實現了ReadWriteLock介面,具體實現這裡不展開,後續會深入原始碼解析。

公平鎖和非公平鎖

公平鎖

公平鎖是指多個執行緒按照申請鎖的順序來獲取鎖,這裡類似排隊買票,先來的人先買,後來的人在隊尾排著,這是公平的。

20210606232716-2021-06-06-23-27-17

在 java 中可以通過建構函式初始化公平鎖

/**
* 建立一個可重入鎖,true 表示公平鎖,false 表示非公平鎖。預設非公平鎖
*/
Lock lock = new ReentrantLock(true);

非公平鎖

非公平鎖是指多個執行緒獲取鎖的順序並不是按照申請鎖的順序,有可能後申請的執行緒比先申請的執行緒優先獲取鎖,在高並行環境下,有可能造成優先順序翻轉,或者飢餓的狀態(某個執行緒一直得不到鎖)。

20210606232737-2021-06-06-23-27-38

在 java 中 synchronized 關鍵字是非公平鎖,ReentrantLock預設也是非公平鎖。

/**
* 建立一個可重入鎖,true 表示公平鎖,false 表示非公平鎖。預設非公平鎖
*/
Lock lock = new ReentrantLock(false);

可重入鎖

可重入鎖又稱之為遞迴鎖,是指同一個執行緒在外層方法獲取了鎖,在進入內層方法會自動獲取鎖。

20210606232755-2021-06-06-23-27-56

對於Java ReentrantLock而言, 他的名字就可以看出是一個可重入鎖。對於Synchronized而言,也是一個可重入鎖。

敲黑板:可重入鎖的一個好處是可一定程度避免死鎖。

以 synchronized 為例,看一下下面的程式碼:

public synchronized void mehtodA() throws Exception{
 // Do some magic tings
 mehtodB();
}

public synchronized void mehtodB() throws Exception{
 // Do some magic tings
}

上面的程式碼中 methodA 呼叫 methodB,如果一個執行緒呼叫methodA 已經獲取了鎖再去呼叫 methodB 就不需要再次獲取鎖了,這就是可重入鎖的特性。如果不是可重入鎖的話,mehtodB 可能不會被當前執行緒執行,可能造成死鎖。

自旋鎖

自旋鎖是指執行緒在沒有獲得鎖時不是被直接掛起,而是執行一個忙迴圈,這個忙迴圈就是所謂的自旋。

20210606232809-2021-06-06-23-28-09

自旋鎖的目的是為了減少執行緒被掛起的機率,因為執行緒的掛起和喚醒也都是耗資源的操作。

如果鎖被另一個執行緒佔用的時間比較長,即使自旋了之後當前執行緒還是會被掛起,忙迴圈就會變成浪費系統資源的操作,反而降低了整體效能。因此自旋鎖是不適應鎖佔用時間長的並行情況的。

在 Java 中,AtomicInteger 類有自旋的操作,我們看一下程式碼:

public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = getIntVolatile(o, offset);
    } while (!compareAndSwapInt(o, offset, v, v + delta));
    return v;
}

CAS 操作如果失敗就會一直迴圈獲取當前 value 值然後重試。

另外自適應自旋鎖也需要了解一下。

在JDK1.6又引入了自適應自旋,這個就比較智慧了,自旋時間不再固定,由前一次在同一個鎖上的自旋時間以及鎖的擁有者的狀態來決定。如果虛擬機器器認為這次自旋也很有可能再次成功那就會次序較多的時間,如果自旋很少成功,那以後可能就直接省略掉自旋過程,避免浪費處理器資源。

分段鎖

分段鎖 是一種鎖的設計,並不是具體的一種鎖。

分段鎖設計目的是將鎖的粒度進一步細化,當操作不需要更新整個陣列的時候,就僅僅針對陣列中的一項進行加鎖操作。

20210606232830-2021-06-06-23-28-31

在 Java 語言中 CurrentHashMap 底層就用了分段鎖,使用Segment,就可以進行並行使用了。

鎖升級(無鎖|偏向鎖|輕量級鎖|重量級鎖)

JDK1.6 為了提升效能減少獲得鎖和釋放鎖所帶來的消耗,引入了4種鎖的狀態:無鎖偏向鎖輕量級鎖重量級鎖,它會隨著多執行緒的競爭情況逐漸升級,但不能降級。

無鎖

無鎖狀態其實就是上面講的樂觀鎖,這裡不再贅述。

偏向鎖

Java偏向鎖(Biased Locking)是指它會偏向於第一個存取鎖的執行緒,如果在執行過程中,只有一個執行緒存取加鎖的資源,不存在多執行緒競爭的情況,那麼執行緒是不需要重複獲取鎖的,這種情況下,就會給執行緒加一個偏向鎖。

偏向鎖的實現是通過控制物件Mark Word的標誌位來實現的,如果當前是可偏向狀態,需要進一步判斷物件頭儲存的執行緒 ID 是否與當前執行緒 ID 一致,如果一致直接進入。

輕量級鎖

當執行緒競爭變得比較激烈時,偏向鎖就會升級為輕量級鎖,輕量級鎖認為雖然競爭是存在的,但是理想情況下競爭的程度很低,通過自旋方式等待上一個執行緒釋放鎖。

重量級鎖

如果執行緒並行進一步加劇,執行緒的自旋超過了一定次數,或者一個執行緒持有鎖,一個執行緒在自旋,又來了第三個執行緒存取時(反正就是競爭繼續加大了),輕量級鎖就會膨脹為重量級鎖,重量級鎖會使除了此時擁有鎖的執行緒以外的執行緒都阻塞。

升級到重量級鎖其實就是互斥鎖了,一個執行緒拿到鎖,其餘執行緒都會處於阻塞等待狀態。

在 Java 中,synchronized 關鍵字內部實現原理就是鎖升級的過程:無鎖 --> 偏向鎖 --> 輕量級鎖 --> 重量級鎖。這一過程在後續講解 synchronized 關鍵字的原理時會詳細介紹。

鎖優化技術(鎖粗化、鎖消除)

鎖粗化

鎖粗化就是將多個同步塊的數量減少,並將單個同步塊的作用範圍擴大,本質上就是將多次上鎖、解鎖的請求合併為一次同步請求。

舉個例子,一個迴圈體中有一個程式碼同步塊,每次迴圈都會執行加鎖解鎖操作。

private static final Object LOCK = new Object();

for(int i = 0;i < 100; i++) {
    synchronized(LOCK){
        // do some magic things
    }
}

經過鎖粗化後就變成下面這個樣子了:

 synchronized(LOCK){
     for(int i = 0;i < 100; i++) {
        // do some magic things
    }
}

鎖消除

鎖消除是指虛擬機器器編譯器在執行時檢測到了共用資料沒有競爭的鎖,從而將這些鎖進行消除。

舉個例子讓大家更好理解。

public String test(String s1, String s2){
    StringBuffer stringBuffer = new StringBuffer();
    stringBuffer.append(s1);
    stringBuffer.append(s2);
    return stringBuffer.toString();
}

上面程式碼中有一個 test 方法,主要作用是將字串 s1 和字串 s2 串聯起來。

test 方法中三個變數s1, s2, stringBuffer, 它們都是區域性變數,區域性變數是在棧上的,棧是執行緒私有的,所以就算有多個執行緒存取 test 方法也是執行緒安全的。

我們都知道 StringBuffer 是執行緒安全的類,append 方法是同步方法,但是 test 方法本來就是執行緒安全的,為了提升效率,虛擬機器器幫我們消除了這些同步鎖,這個過程就被稱為鎖消除

StringBuffer.class

// append 是同步方法
public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
}

一張圖總結:

前面講了 Java 語言中各種各種的鎖,最後再通過六個問題統一總結一下:

Java中那些眼花繚亂的鎖-2021-06-16-23-19-40

推薦學習:《》

以上就是完全掌握Java鎖(圖文解析)的詳細內容,更多請關注TW511.COM其它相關文章!