1. 程式人生 > >一文帶你理解Java中Lock的實現原理

一文帶你理解Java中Lock的實現原理

當多個執行緒需要訪問某個公共資源的時候,我們知道需要通過加鎖來保證資源的訪問不會出問題。java提供了兩種方式來加鎖,一種是關鍵字:synchronized,一種是concurrent包下的lock鎖。synchronized是java底層支援的,而concurrent包則是jdk實現。關於synchronized的原理可以閱讀再有人問你synchronized是什麼,就把這篇文章發給他。

在這裡,我會用盡可能少的程式碼,儘可能輕鬆的文字,儘可能多的圖來看看lock的原理。

我們以ReentrantLock為例做分析,其他原理類似。

我把這個過程比喻成一個做菜的過程,有什麼菜,做法如何?

我先列出lock實現過程中的幾個關鍵詞:計數值、雙向連結串列、CAS+自旋

使用例子

import java.util.concurrent.locks.ReentrantLock;

public class App {

    public static void main(String[] args) throws Exception {
        final int[] counter = {0};

        ReentrantLock lock = new ReentrantLock();

        for (int i= 0; i < 50; i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    lock.lock();
                    try {
                        int a = counter[0];
                        counter[0] = a + 1;
                    }finally {
                        lock.unlock();
                    }
                }
            }).start();
        }

        // 主執行緒休眠,等待結果
        Thread.sleep(5000);
        System.out.println(counter[0]);
    }
}

在這個例子中,開50個執行緒同時更新counter。分成三塊來看看原始碼(初始化、獲取鎖、釋放鎖)

實現原理

ReentrantLock() 幹了啥

 /**
     * Creates an instance of {@code ReentrantLock}.
     * This is equivalent to using {@code ReentrantLock(false)}.
     */
    public ReentrantLock() {
        sync = new NonfairSync();
    }

在lock的建構函式中,定義了一個NonFairSync,

static final class NonfairSync extends Sync 

NonfairSync 又是繼承於Sync

abstract static class Sync extends AbstractQueuedSynchronizer

一步一步往上找,找到了 這個鬼AbstractQueuedSynchronizer(簡稱AQS),最後這個鬼,又是繼承於AbstractOwnableSynchronizer(AOS),AOS主要是儲存獲取當前鎖的執行緒物件,程式碼不多不再展開。 最後我們可以看到幾個主要類的繼承關係。

鎖的類的繼承關係

FairSync 與 NonfairSync的區別在於,是不是保證獲取鎖的公平性,因為預設是NonfairSync,我們以這個為例瞭解其背後的原理。

其他幾個類程式碼不多,最後的主要程式碼都是在AQS中,我們先看看這個類的主體結構。

AbstractQueuedSynchronizer是個什麼

再看看Node是什麼?

看到這裡的同學,是不是有種熱淚盈眶的感覺,這尼瑪,不就是雙向連結串列麼?我還記得第一次寫這個資料結構的時候,發現居然還有這麼神奇的一個東西。

最後我們可以發現鎖的儲存結構就兩個東西:"雙向連結串列" + "int型別狀態"。 需要注意的是,他們的變數都被"transientvolatile修飾。

一個int值,一個雙向連結串列是如何烹飪處理鎖這道菜的呢,Doug Lea大神就是大神,我們接下來看看,如何獲取鎖?

lock.lock()怎麼獲取鎖?

/**
 * Acquires the lock.
 */
public void lock() {
    sync.lock();
}

可以看到呼叫的是,NonfairSync.lock()

看到這裡,我們基本有了一個大概的瞭解,還記得之前AQS中的int型別的state值,這裡就是通過CAS(樂觀鎖)去修改state的值。lock的基本操作還是通過樂觀鎖來實現的

獲取鎖通過CAS,那麼沒有獲取到鎖,等待獲取鎖是如何實現的?我們可以看一下else分支的邏輯,acquire方法:

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

這裡幹了三件事情:

  • tryAcquire:會嘗試再次通過CAS獲取一次鎖。

  • addWaiter:將當前執行緒加入上面鎖的雙向連結串列(等待佇列)中

  • acquireQueued:通過自旋,判斷當前佇列節點是否可以獲取鎖。

addWaiter 添加當前執行緒到等待連結串列中

可以看到,通過CAS確保能夠線上程安全的情況下,將當前執行緒加入到連結串列的尾部。 enq是個自旋+上述邏輯,有興趣的可以翻翻原始碼。

acquireQueued

自旋+CAS嘗試獲取鎖 可以看到,噹噹前執行緒到頭部的時候,嘗試CAS更新鎖狀態,如果更新成功表示該等待執行緒獲取成功。從頭部移除。

每一個執行緒都在自旋+CAS

最後簡要概括一下,獲取鎖的一個流程

獲取鎖流程

lock.unlock() 釋放鎖

public void unlock() {
    sync.release(1);
}

可以看到呼叫的是,NonfairSync.release()

最後有呼叫了NonfairSync.tryRelease()

基本可以確認,釋放鎖就是對AQS中的狀態值State進行修改。同時更新下一個連結串列中的執行緒等待節點。

總結

  • lock的儲存結構:一個int型別狀態值(用於鎖的狀態變更),一個雙向連結串列(用於儲存等待中的執行緒)

  • lock獲取鎖的過程:本質上是通過CAS來獲取狀態值修改,如果當場沒獲取到,會將該執行緒放線上程等待連結串列中。

  • lock釋放鎖的過程:修改狀態值,調整等待連結串列。

  • 可以看到在整個實現過程中,lock大量使用CAS+自旋。因此根據CAS特性,lock建議使用在低鎖衝突的情況下。目前java1.6以後,官方對synchronized做了大量的鎖優化(偏向鎖、自旋、輕量級鎖)。因此在非必要的情況下,建議使用synchronized做同步操作。