ReentrantLock從結構底層到原理一篇講清

ReentrantLock

1.是什麼

Reetrantlock是一個可以重複獲得鎖的一個互斥鎖,它的加鎖與解鎖都是需要手動執行,也可以多次加鎖,同時它還可以指定是由公平鎖還是非公平鎖實現。

2.內部實現

img

構造方法:

 
public ReentrantLock() {
        sync = new NonfairSync();
    }

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

由構造方法可知,reentrantlock是由公平鎖和非公平鎖實現的,而公平鎖和非公平鎖都是繼承實現AQS(AbstractQueuedSynchronizer)的模板方法和底層原理得以實現

AbstractQueuedSynchronizer(AQS)

//節點元素node的構成:
volatile Node prev; //指向前一個結點的指針
volatile Node next; //指向後一個結點的指針
volatile Thread thread; //當前結點代表的線程
volatile int waitStatus; //等待狀態

//AQS其他的數據結構
private volatile int state;//Reentratlock中表示鎖被同一個線程重入的次數
private transient Thread exclusiveOwnerThread;//標識鎖是由哪個線程擁有

img

這裏的頭指針的頭結點表示當前獲得這個鎖的線程,後面的節點表示想要獲得這個鎖的線程。這是一個先進先出的同步隊列,獲取鎖失敗的線程將構造自己的節點並追加到隊列的尾部,並且阻塞自己,而當線程釋放鎖之後,也將嘗試喚醒後續節點中處於阻塞狀態的線程。

AQS的底層結構式lock實現的重要基礎,對於公平鎖和非公平鎖來說,只是方法執行邏輯不同。

非公平鎖NonfairSync

繼承Sync,實現了lock和tryAcquire方法

 final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }

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


 final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            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;
        }

lock方法是默認去用cas的方式去更新state和當前持有線程,如果AQS中state爲0,也就是隊列爲空,那麼設置state和當前線程就可以,如果不成功,就加入隊列。

Reentratlock非公平鎖的加鎖流程

img

1.1,嘗試獲取鎖首先獲取AQS中state的值,如果爲0,表示當前鎖沒有被任何線程持有,以cas的方式設置state的值,以獲取鎖,並設置exclusiveOwnerThread標識這個鎖由哪個線程擁有

1.2 如果state值不爲0,則看當前鎖的持有線程是否是自己,如果是,就可以重入了,對state加1 就可以了,獲取鎖也成功,如果持有線程不是自己,返回失敗,

2,線程獲取鎖失敗以後,就會爲當前線程創建一個新的節點,判斷tail尾指針是否爲空,如果爲空,構建新的節點同時設置爲首尾指針,如果不是也就是隊列不爲空,那麼就要以尾插方式放入隊列尾部。

//先將新建節點前置節點置爲尾節點,然後以cas的方式更新tail指針,最後新建節點入隊
node.prev = t;
   if (compareAndSetTail(t, node)) {
        t.next = node;
        return t;
   }

3,將新建的節點入隊後,進入一個死循環,只有新建節點的前節點是頭節點並且自己獲取鎖成功以後,將頭結點設置爲自己以後才能跳出循環,否者的話就要,並將前置節點置爲SIGNAL阻塞(調用LockSupport方法)當前線程。

4,隨着隊列不斷的出隊,後面的節點的前節點總會是頭結點,而自己也會獲得鎖,那麼他也會被喚醒。

ReentratLock的解鎖過程

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

public final boolean release(int arg) {
        if (tryRelease(arg)) {//將state減1,如果減1後爲0,返回true
            Node h = head;
    //防止隊列爲空,喚醒前節點必須是阻塞的,而節點阻塞前要設置前節點爲SIGNAL
            if (h != null && h.waitStatus != 0)
               //喚醒線程
                unparkSuccessor(h);
            return true;
        }
        return false;
    }


private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
        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;
            //從爲節點開始遍歷,找到離頭節點最近且節點狀態爲SIGNAL的節點
        }
        if (s != null)
            //使用LockSupport喚醒線程
            LockSupport.unpark(s.thread);
    }

img

當前線程將state值減1,減少爲0後,就表示線程釋放了鎖,就要喚醒後繼節點,使得他可以獲得鎖,而這個後繼節點的狀態waitStatus小於0時纔可以被喚醒

公平鎖FairSync

Reentratlock公平鎖的加鎖流程

        final void lock() {
            acquire(1);
        }

        /**
         * Fair version of tryAcquire.  Don't grant access unless
         * recursive call or no waiters or is first.
         */
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
  //相比較而言,公平鎖的流程在cas更新state前多了一個判斷隊列中是否有更高優先級的節點
                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;
        }

相比較非公平鎖來說首先

1,在lock方法中少了一步直接cas更新state的方法

2,在tryAcquire方法中加入了hasQueuedPredecessor判斷

hasQueuedPredecessor判斷如下

public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        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());
    }

這裏的判斷判斷了兩種情況

1,頭節點的後續節點爲空

2,AQS的持有鎖線程不是當前線程

爲第一種要判斷呢??因爲當一個節點要加入隊列時有三步:

  1. 待插入節點pre指針指向尾節點
  2. cas方式更新AQS尾指針
  3. 原尾節點的next指向新插入節點

第一個判斷是爲了排除前兩步執行完成,但是第三步沒完成的情況,也就是隊列中除了頭節點外,還有後續的第二個節點,它應該第二個獲得鎖,這種情況下,當前線程應該入隊,等待鎖。

第二個判斷就是隊列中有第二個節點且AQS持有鎖的線程爲其他線程

所以這個判斷就是爲了判斷隊列中是否有第二個節點,這個節點獲取鎖的優先級最高,應該讓他先獲得鎖。

爲什麼要對第二節點做判斷

由於底層實現爲同步隊列,隊列也就是先進先出,越早進入隊列的線程應該越先獲得鎖,這樣纔是公平合理的,但是在非公平模式下,我們知道它有兩次cas方式去更新state值和更新AQS持有線程

  1. 在lock方法中首先就嘗試去cas更新state值,更新成功的話,那麼就獲得鎖成功
  2. 在nonfairTryAcquire方法中也有對state值cas方式去更新值的判斷

那麼這兩次判斷真的能獲得鎖嗎?

在釋放鎖的流程中,線程將state更新爲0,然後去喚醒後續節點,就是(LockSupport.unpark(s.thread))找到符合條件的節點解鎖。

這就是兩步,那麼在多線程條件下,第一步執行完之後,此時恰好另外一個線程進入,看到state值爲0,那麼就可直接獲得鎖,更新完state和AQS持有線程後,就獲得了鎖。而這時剛剛解鎖完的節點去喚醒的節點獲取state值時發現不爲0,繼續阻塞等待,這也就造成了不公平。等的時間長不如進來的時間巧。從流程來說就是如下

img

那麼在公平鎖模式下,只要有第二節點的存在,後面進行的線程就必須要加入隊列,而不能直接去cas的方式更新state值,對於多線程來說就是按加入時間長短來獲取鎖,也就實現了公平。

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