小白的 ReentrantLock 源碼略談

開門見山,ReentrantLock 簡單介紹

ReentrantLock(簡稱RLock) 是Java的一種鎖機制。從API上看,RLock提供了公平鎖與非公平鎖,並提供了當前鎖狀態監測的一些接口。其內部是由 FairSyncNonFairSync 來實現鎖資源的搶佔與釋放。下面咱們來學習下其源碼。node

從最直白的方法入手

首先咱們打開 RLock 的構造函數,源碼以下:c#

private final Sync sync;
    public ReentrantLock() {
        sync = new NonfairSync();
    }
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
複製代碼

好的,很直白,根據入參構形成員變量 Sync。默認爲 NonfairSync。到這裏,咱們遇到了第一個新概念 Sync 。先按住好奇心,咱們先找到 lock()unlock() 源碼。以下:bash

public void lock() {
        sync.lock();
    }
    public void unlock() {
        sync.release(1);
    }
複製代碼

到這裏好像有些明白了,RLock更像是對Sync的進一層封裝,經過多態來實現不一樣的鎖策略。到這裏,我有個疑問,公平鎖和非公平鎖的策略有何不一樣呢?那麼這樣看來,啃透Sync是關鍵。併發

居高觀下

首先咱們捋一下 Sync 繼承結構。IDEA裏鼠標移到類聲明上,Ctrl+H便可清晰看到類的繼承結構。 框架

Sync類繼承結構
這裏咱們看到了一個熟悉的老朋友-- AbstractQueuedSynchronizer(簡稱AQS)。這個類是JDK併發包中的鎖基類,定義了鎖資源獲取與釋放的框架與基本行爲。這個先略過不談,繼續貫徹第一步,從最直白的方法入手。

lock()

咱們回憶一下lock的行爲,咱們調用來獲取鎖,若是其它線程已搶佔到鎖資源,當前線程掛起,直到當前線程獲取到鎖。並且RLock支持重入。 NonfairSync#lock()FairSync#lock() 源碼以下: 函數

Sync#lock()
從第4,5,6行能夠看到, NonfairSync 先設置了狀態位,而後調用了 acquire()FairSync 則直接調用了 acquire()。那麼咱們先從 compareAndSetState() 入手,源碼以下:

protected final boolean compareAndSetState(int expect, int update) {
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }
複製代碼

這個函數定義在 AQS 中,用於設置當前lock的狀態,unsafe呢,是JDK中很是變態的一個工具類,能夠直接操控實例對象所在的內存,同時提供了一些原子操做。具體的後面會再展開介紹,簡而言之,這個函數能夠理解爲:工具

protected final boolean compareAndSetState(int expect, int update) {
        synchronized (this.getClass()) {
            if (this.state == expect) {
                this.state = update;
                return true;
            } else {
                return false;
            }
        }
    }
複製代碼

那麼到這裏咱們好像獲得了第一把鑰匙:lock.state 爲0時,爲空閒,而上鎖請求會將狀態置爲1,而且將exclusiveOwnerThread設爲當前線程。學習

接下來,咱們來看 acquire()。繼續跟蹤下去, 的 acquire() 代碼以下:ui

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

唔,好像看不出來什麼東西,那麼繼續跟下去,先從 tryAcquire 開始,NonfairSyncFairSync tryAcquire() 核心代碼以下 this

tryAcquire()核心代碼對比
惟一的差異在於, FairSync 獲取鎖以前會調用 hasQueuedPredecessors() 源碼以下:

/**
     * @return {@code true} if there is a queued thread preceding the
     *         current thread, and {@code false} if the current thread
     *         is at the head of the queue or the queue is empty
     *  兩種狀況返回 false:1. 當前線程在隊頭。2. 隊列爲空
     */
    public final boolean hasQueuedPredecessors() {
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }
複製代碼

那麼咱們拿到了第二把鑰匙: 公平鎖在獲取鎖以前會先去查詢是否有其餘人在等待這把鎖,若是沒有,再嘗試獲取。而非公平鎖則不會詢問

這麼看來,公平鎖 Peace&Love,像民謠,多愁善感,與世無爭。而非公平鎖 Aggressive,像Hip-Hop,張揚自我,銳意進取。 那麼回到 acquire() ,還有一個函數: acquireQueued(),源碼以下:

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
複製代碼

忽略其餘細節,咱們在 parkAndCheckInterrupt() 函數中找到了 LockSupport.park(this);。就是他了!這個函數會將指定線程掛起,直至LockSupport.unpark(Thread)被調用或者發生意外被操做系統 interrupt。 至此, lock() 的鏈便通了。

咱們再來總結一下:***ReentrantLock在上鎖時,會根據實例化時指定的策略去獲取鎖,默認爲非公平鎖。若是上鎖成功,鎖狀態值+1(重入,最大次數爲 Integer.MAX_VALUE),並將鎖持有者設置爲當前線程實例。在 Sync 內部維護了一個隊列,存放了全部上鎖失敗的線程。公平鎖在上鎖前,會檢查在本身前面是否還有其餘線程等待,若是有就放棄競爭,繼續等待。而非公平鎖會抓住每一個機會,無論是否前面是否還有其它線程等待,只顧上鎖***

unlock()

至於鎖釋放,公平鎖與非公平鎖的行爲就同樣了。核心代碼以下

// ReentrantLock#unlock() 釋放鎖資源
    public void unlock() {
        sync.release(1);
    }
    // Sync#release()
    public final boolean release(int arg) {
        // 重入鎖,狀態計數器減一,爲0時釋放
        if (tryRelease(arg)) { 
            Node h = head;
            // 釋放鎖時,從等待隊列中獲取線程並嘗試喚醒
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
    // Sync#tryRelease() 狀態計數器減一,爲0時,釋放鎖資源,返回true
    protected final boolean tryRelease(int releases) {
        int c = getState() - releases;
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false;
        if (c == 0) {
            free = true;
            setExclusiveOwnerThread(null);
        }
        setState(c);
        return free;
    }
    // Sync#unparkSuccessor() 喚醒等待隊列中的線程,讓他(們)繼續搶佔鎖
    private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            // 上鎖時向隊列尾部添加元素時,可能會致使隊列處在中間狀態,再從尾部遍歷一次
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        // boomya,合適的線程找到啦,將其喚醒
        if (s != null)
            LockSupport.unpark(s.thread);
    }
複製代碼

總結一下: 線程在釋放鎖時,將狀態計數器減一(重入),當狀態計數器爲0時,鎖可用。此時再從等待隊列中尋找合適的線程喚醒,默認從隊首開始,若是隊列正在更新中,且未找到合適的線程,那麼從隊尾開始尋找。


重複的總結,就知道你喜歡看結論

  • ReentrantLock在上鎖時,會根據實例化時指定的策略去獲取鎖,默認爲非公平鎖。若是上鎖成功,鎖狀態值+1(重入,最大次數爲 Integer.MAX_VALUE),並將鎖持有者設置爲當前線程實例。在 Sync 內部維護了一個隊列,存放了全部上鎖失敗的線程。公平鎖在上鎖前,會檢查在本身前面是否還有其餘線程等待,若是有就放棄競爭,繼續等待。而非公平鎖會抓住每一個機會,無論是否前面是否還有其它線程等待,只顧上鎖
  • ReetrantLock在釋放鎖時,將狀態計數器減一(重入),當狀態計數器爲0時,鎖可用。此時再從等待隊列中尋找合適的線程喚醒,默認從隊首開始,若是隊列正在更新中,且未找到合適的線程,那麼從隊尾開始尋找。
相關文章
相關標籤/搜索