ReentrantLock 的實現原理以及源碼分析

ReentrantLock是一個可重入的鎖,內部採用AQS來實現

下面我會對其源碼進行詳細的分析,因爲貼了源碼,所以我會直接在源碼上進行註釋,並加入一些我自己的理解,以求大家可以更容易的理解。廢話不多說,直接開始:

ReentrantLock的構造函數:

 

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

可以看到構造函數做了一件事,就是將lock實例的sync字段進行了初始化,從代碼可以看出根據參數的不同,將sync初始化爲不同的實現,即公平鎖FairSync和非公平鎖NonfairSync,那sync到底是什麼呢?

 

abstract static class Sync extends AbstractQueuedSynchronizer {}

具體的代碼就不粘出來了,可以看到Sync繼承自AbstractQueuedSynchronizer抽象隊列同步器AQS,也就是說ReentrantLock的真正實現其實是依賴了AQS,下面就對AQS進行分析

 

首先看一下AQS中比較重要的屬性

exclusiveOwnerThread:獨佔鎖線程,指向了當前獲取到鎖的線程

state:AQS的核心,AQS就是用這個字段來實現鎖的獲取和重入,在沒有線程獲取到鎖的時候,鎖的狀態爲0,獲取的時候,通過cas對其進行+1,並且每重入一次再 +1,釋放一次 -1,具體的後面代碼展示

head,tail:AQS維護了一個內部類Node的雙向隊列,由未獲取到鎖的線程包裝成的Node節點組成,也就是獲取鎖失敗加入隊列尾部。

 

AQS的獲取鎖過程分析

線程通過CAS將state從0設置爲1,如果設置成功,說明獲取鎖成功,並且將exclusiveOwnerThread指向自己;

當調用lock方法時,公平鎖和非公平鎖的實現有區別

非公平鎖的實現:線程會直接進行CAS操作去設置state,如果成功就獲取到鎖,如果失敗,繼續調用NonFairSync的acquire(1)方法

公平鎖的實現:線程會直接調用FairSync的accquire(1)方法,這兩個方法都調用了AQS的方法

 

public final void acquire(int arg) {
    //第一步嘗試獲取鎖
    //如果成功了,就可以執行自身的邏輯了
    //如果失敗了,執行下一步addWaiter()方法,也就是將自身線程包裝成Node加入到隊列中
    //第三步 死循環獲取鎖,如果失敗則將線程掛起
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

這裏需要看一下tryAcquire方法,首先是非公平鎖

 

final boolean nonfairTryAcquire(int acquires) {
    //獲取當前線程
    final Thread current = Thread.currentThread();
    //獲取AQS的狀態
    int c = getState();
    //如果等於0,說明沒有線程獲取到鎖,通過CAS嘗試獲取鎖,跟一開始的邏輯一模一樣
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //如果不等於0,說明有線程已經獲取到鎖了,這裏判斷AQS的獨佔鎖指針是不是指向自己
    //如果是,說明是重入,這裏就是重入的實現
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    //否則不是自己,那麼就直接返回獲取鎖失敗
    return false;
}

下面看一下公平鎖的實現

 

protected final boolean tryAcquire(int acquires) {
    //前面的邏輯都一樣
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        //差別就在這裏,多了一個hasQueuedPredecessors方法,這是跟非公平鎖唯一的區別
        //也就是爲什麼叫公平鎖體現在這裏。
        //這裏判斷有沒有別的線程比他更早來,如果返回true說明有,肯定就直接獲取失敗了
        //如果沒有別他更早來的線程,那麼自己就可以去嘗試獲取鎖了
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

下面看一下hasQueuedPredecessors方法

 

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());
}

總結一下,AQS中維護了一堆wait線程組成的等待隊列,凡是進入了這個隊列的線程,之後就會按順序一個一個獲取到鎖,執行邏輯,也就是將他們串行化了,那麼公平和非公平體現在什麼地方呢?

就是tryAcquire這裏,公平鎖是說新線程進來對於隊列中的線程是公平的,如果隊列中有等待線程,它就直接往後排,而非公平鎖是,新線程對於隊列中的等待線程是不公平的,可能存在隊列中的頭節點釋放掉鎖之後喚醒下一個線程,結果有一個新的線程進來同時獲取鎖,這個時候他們機會是平等的,因此說這是非公平鎖。

 

下面看一下addWaiter方法

 

private Node addWaiter(Node mode) {
    //將當前線程封裝成node
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;
    //如果tail不爲空,也就是隊列中已經有一些等待線程了,直接加入尾部後返回即可。
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    //如果tail爲空,就會走到這裏來
    enq(node);
    return node;
}
private Node enq(final Node node) {
    //死循環,爲什麼死循環呢?往下看
    for (;;) {
        //拿到tail,第一次循環這裏肯定是null
        Node t = tail;
        if (t == null) { // Must initialize
            //第一次進來,將一個空的節點作爲頭和尾節點,看一下Node的構造函數和屬性
            //可以發現頭節點的waitstatus=0
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            //第二次進來就到了這裏
            node.prev = t;
            //將node節點插入到隊列的最後
            //這裏爲什麼這麼做呢?向下看
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

解答上面的問題,能這麼設計就有他的原因,看一下死循環獲取鎖的方法

 

final boolean acquireQueued(final Node node, int arg) {
    
    boolean failed = true;
    try {
        boolean interrupted = false;
        //這裏是死循環,是核心
        for (;;) {
            //獲取node節點的prev節點,如果沒有直接報錯
            final Node p = node.predecessor();
            //判斷p是不是頭節點,如果是,嘗試獲取鎖
            //如果能夠成功,說明head要麼取消,要麼已經釋放了鎖
            //所以將當前節點設置爲頭節點,也就是說刪除頭節點的操作是在這裏執行的
            //並不是在釋放鎖的時候執行的
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // 將node的狀態改爲singal之後掛起,等待有線程喚醒他之後
            // 就可以繼續循環獲取鎖了
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        //這裏就是線程可能會被中斷,如果中斷了就從隊列中去除掉
        if (failed)
            cancelAcquire(node);
    }
}
//node節點獲取鎖失敗就會進入這個方法,第一次進入的時候,pred是頭節點,並且ws=0

//這個方法的源碼先看else塊,再看第一個if,再看第二個if
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            
            //第二次走這裏
            //直接返回true,那麼就會調用park方法,線程阻塞
            return true;
        if (ws > 0) {

            //這裏就是當node的前置節點不是頭節點並且狀態是已取消狀態
            //那麼就是去找下一個正常的前置節點,這裏就是維護一下鏈表的關係
            //之後return false 同樣是要進行二次循環
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             */
            //第一次進來走這裏,把頭節點的ws設置爲-1,然後return false
            //也就是外面的循環邏輯會走第二遍,那麼第二遍進來的時候,頭節點的ws已經是-1了    
            //就會走第一個if
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

 

釋放鎖的原理

 

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

public final boolean release(int arg) {
    //嘗試釋放鎖,成功之後,準備喚醒頭節點的下一個正常節點
    if (tryRelease(arg)) {
        Node h = head;
        //如何代碼沒有看仔細,會對頭節點的waitStatus狀態有疑惑
        //以爲頭節點是一個空姐點,初始狀態爲0
        //但是經過了shouldParkAfterFailedAcquire方法之後,狀態就是變成-1
        //所以這裏是true,正常進行釋放操作
        if (h != null && h.waitStatus != 0)
            //這個方法就是真正的喚醒頭節點的下一個正常的節點
            unparkSuccessor(h);
        return true;
    }
    return false;
}

protected final boolean tryRelease(int releases) {
    //重入性質
    int c = getState() - releases;
    //如果當前線程不是AQS的獨佔鎖線程直接報錯
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    //直到全部釋放完成,將獨佔鎖線程設置爲null,釋放成功
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    //重入鎖就一直 -- ,然後CAS設置狀態就好了
    setState(c);
    return free;
}

private void unparkSuccessor(Node node) {
   //獲取頭節點的狀態
    int ws = node.waitStatus;
    if (ws < 0)
        //這裏將頭節點的狀態變成0
        //等待後置節點將其替換
        compareAndSetWaitStatus(node, ws, 0);

    // 獲取頭節點的下一個節點
    Node s = node.next;
    // s如果是null,或者狀態是已取消的狀態,就從尾部獲取下一個正常的可以喚醒的節點
    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;
    }
    // 通過上面的計算獲取到的節點,在這裏進行喚醒
    // 存在當前node既是head也是tail的情況,這裏的thread就是null
    if (s != null)
        LockSupport.unpark(s.thread);
}

 

整個ReentrantLock的源碼分析就是這樣,筆者在看了這些源碼大概幾十次之後(但是都是粗略看的,慚愧,認真看也就是兩次,一次是開始寫這篇文章,另外一次就是修改第一次寫的時候一些錯誤的地方),終於看懂了,其實不是很難,相反理解之後反而覺得非常之簡單,自身的收穫也很大。歡迎大家對我的分析進行指正,大家一起進步!!!

 

原創不易,且行且珍惜。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章