Java併發編程原理與實戰十七:AQS實現重入鎖

1、什麼是重入鎖

    可重入鎖就是當前持有鎖的線程可以屢次獲取該鎖,無需等待java

2、什麼是AQS

 AQS是JDK1.5提供的一個基於FIFO等待隊列實現的一個用於實現同步器的基礎框架,這個基礎框架的重要性能夠這麼說,JCU包裏面幾乎全部的有關鎖、多線程併發以及線程同步器等重要組件的實現都是基於AQS這個框架。AQS的核心思想是基於volatile int state這樣的一個屬性同時配合Unsafe工具對其原子性的操做來實現對當前鎖的狀態進行修改。當state的值爲0的時候,標識改Lock不被任何線程所佔有。node

3、ReentrantLock鎖的架構

 ReentrantLock鎖主要包括一個Sync的內部抽象類以及Sync抽象類的兩個實現類多線程

                 

 AQS的父類AbstractOwnableSynchronizer(後面簡稱AOS),AOS主要提供一個exclusiveOwnerThread屬性,用於關聯當前持有該鎖的線程。另外、Sync的兩個實現類分別是NonfairSync和FairSync架構

4、AQS的等待隊列

假設目前有三個線程Thread一、Thread二、Thread3同時去競爭鎖,若是結果是Thread1獲取了鎖,Thread2和Thread3進入了等待隊列,那麼他們的樣子以下:併發

             

AQS的等待隊列基於一個雙向鏈表實現的,HEAD節點不關聯線程,後面兩個節點分別關聯Thread2和Thread3,他們將會按照前後順序被串聯在這個隊列上。這個時候若是後面再有線程進來的話將會被當作隊列的TAIL。框架

一、入隊列分佈式

當這三個線程同時去競爭鎖的時候發生了什麼ide

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

三個線程同時進來,他們會首先會經過CAS去修改state的狀態,若是修改爲功,那麼競爭成功,所以這個時候三個線程只有一個CAS成功,其餘兩個線程失敗,也就是tryAcquire返回false。工具

 

接下來,addWaiter會把將當前線程關聯的EXCLUSIVE類型的節點入隊列:oop

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

若是隊尾節點不爲null,則說明隊列中已經有線程在等待了,那麼直接入隊尾。對於咱們舉的例子,這邊的邏輯應該是走enq,也就是開始隊尾是null,其實這個時候整個隊列都是null的

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

若是Thread2和Thread3同時進入了enq,同時t==null,則進行CAS操做對隊列進行初始化,這個時候只有一個線程可以成功,而後他們繼續進入循環,第二次都進入了else代碼塊,這個時候又要進行CAS操做,將本身放在隊尾,所以這個時候又是隻有一個線程成功,咱們假設是Thread2成功,哈哈,Thread2開心的返回了,Thread3失落的再進行下一次的循環,最終入隊列成功,返回本身。

二、併發問題

基於上面兩段代碼,他們是如何實現不進行加鎖,當有多個線程,或者說不少不少的線程同時執行的時候,怎麼能保證最終他們都可以乖乖的入隊列而不會出現併發問題的呢?這也是這部分代碼的經典之處,多線程競爭,熱點、單點在隊列尾部,多個線程都經過【CAS+死循環】這個free-lock黃金搭檔來對隊列進行修改,每次可以保證只有一個成功,若是失敗下次重試,若是是N個線程,那麼每一個線程最多loop N次,最終都可以成功。

三、掛起等待的線程

節點入隊列以後會繼續發生什麼呢?那就要看看acquireQueued是怎麼實現的了,爲保證文章整潔,代碼我就不貼了,同志們自行查閱,咱們仍是以上面的例子來看看,Thread2和Thread3已經被放入隊列了,進入acquireQueued以後:

  1. 對於Thread2來講,它的prev指向HEAD,所以會首先再嘗試獲取鎖一次,若是失敗,則會將HEAD的waitStatus值爲SIGNAL,下次循環的時候再去嘗試獲取鎖,若是仍是失敗,且這個時候prev節點的waitStatus已是SIGNAL,則這個時候線程會被經過LockSupport掛起。

  2. 對於Thread3來講,它的prev指向Thread2,所以直接看看Thread2對應的節點的waitStatus是否爲SIGNAL,若是不是則將它設置爲SIGNAL,再給本身一次去看看本身有沒有資格獲取鎖,若是Thread2仍是擋在前面,且它的waitStatus是SIGNAL,則將本身掛起。

若是Thread1死死的握住鎖不放,那麼Thread2和Thread3如今的狀態就是掛起狀態啦,並且HEAD,以及Thread的waitStatus都是SIGNAL,儘管他們在整個過程當中曾經數次去嘗試獲取鎖,可是都失敗了,失敗了不能死循環呀,因此就被掛起了。當前狀態以下:

            

四、鎖釋放-等待線程喚起    

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

首先,Thread1會修改AQS的state狀態,加入以前是1,則變爲0,注意這個時候對於非公平鎖來講是個很好的插入機會,舉個例子,若是鎖是公平鎖,這個時候來了Thread4,那麼這個鎖將會被Thread4搶去。。。

咱們繼續走常規路線來分析,當Thread1修改完狀態了,判斷隊列是否爲null,以及隊頭的waitStatus是否爲0,若是waitStatus爲0,說明隊列無等待線程,按照咱們的例子來講,隊頭的waitStatus爲SIGNAL=-1,所以這個時候要通知隊列的等待線程,能夠來拿鎖啦,這也是unparkSuccessor作的事情,unparkSuccessor主要作三件事情:

  1. 將隊頭的waitStatus設置爲0.

  2. 經過從隊列尾部向隊列頭部移動,找到最後一個waitStatus<=0的那個節點,也就是離隊頭最近的沒有被cancelled的那個節點,隊頭這個時候指向這個節點。

  3. 將這個節點喚醒,其實這個時候Thread1已經出隊列了。

還記得線程在哪裏掛起的麼,上面說過了,在acquireQueued裏面,我沒有貼代碼,本身去看哦。這裏咱們也大概能理解AQS的這個隊列爲何叫FIFO隊列了,所以每次喚醒僅僅喚醒隊頭等待線程,讓隊頭等待線程先出。

5、AQS實現一個重入鎖

package com;
 
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
 
public class MyLock implements Lock {
    private Helper helper = new Helper();
 
    private class Helper extends AbstractQueuedSynchronizer {
        @Override
        protected boolean tryAcquire(int arg) {
            // 第一個線程進來,能夠獲取鎖
            // 第二個線程進來,沒法獲取鎖,返回false
            Thread thread = Thread.currentThread();
            // 判斷是否爲第一個線程進來
            int state = getState();
            if (state == 0) {
                if (compareAndSetState(0, arg)) {// 若是當前狀態值等於預期值,則以原子方式將同步狀態設置爲給定的更新值
                    // 設置當前線程
                    setExclusiveOwnerThread(Thread.currentThread());
                    return true;
                }
            } else if(getExclusiveOwnerThread() == thread) { // 容許重入鎖,當前線程和當前保存的線程是同一個線程
                setState(state + 1);
                return true;
            }
            return false;
        }
 
        /***
         * 釋放鎖
              此方法老是由正在執行釋放的線程調用。
         */
        @Override
        protected boolean tryRelease(int arg) {
            // 鎖的獲取和釋放確定是一一對應的,那麼調用此方法的線程必定是當前線程
            if (Thread.currentThread() != getExclusiveOwnerThread()) {
                throw new RuntimeException();
            }
            
            boolean flag = false;
            int state = getState() -arg;
            if (state == 0) {// 當前鎖的狀態正確
                setExclusiveOwnerThread(null);
                flag = true;
            }
            setState(state);
            return flag;
        }
 
        protected Condition newCondition() {
            return new ConditionObject();
        }
    }
 
    @Override
    public void lock() {
        // 獨佔鎖
        helper.acquire(1);
    }
 
    @Override
    public void lockInterruptibly() throws InterruptedException {
        // 可中斷
        helper.acquireInterruptibly(1);
    }
 
    @Override
    public boolean tryLock() {
        return helper.tryAcquire(1);
    }
 
    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return helper.tryAcquireNanos(1, unit.toNanos(time));
    }
 
    @Override
    public void unlock() {
        helper.release(1);
    }
 
    @Override
    public Condition newCondition() {
        return helper.newCondition();
    }
}

6、羊羣效應

當有多個線程去競爭同一個鎖的時候,假設鎖被某個線程佔用,那麼若是有成千上萬個線程在等待鎖,有一種作法是同時喚醒這成千上萬個線程去去競爭鎖,這個時候就發生了羊羣效應,海量的競爭必然形成資源的劇增和浪費,所以終究只能有一個線程競爭成功,其餘線程仍是要老老實實的回去等待。AQS的FIFO的等待隊列給解決在鎖競爭方面的羊羣效應問題提供了一個思路:保持一個FIFO隊列,隊列每一個節點只關心其前一個節點的狀態,線程喚醒也只喚醒隊頭等待線程。其實這個思路已經被應用到了分佈式鎖的實踐中,見:Zookeeper分佈式鎖的改進實現方案。

相關文章
相關標籤/搜索