併發系列(八)-----AQS詳解獨佔模式資源的獲取與釋放

一 簡介

上一篇總結了AQS的整體架構,以及它的組成,和它們之間的關係。AQS主要的三部分分別是volatile修飾的變量、同步隊列和等待隊列其中,同步隊列在上篇總結中已經介紹過了,不知道的話可以點這裏AQS的框架組成以及同步隊列源碼解析。這一片文章主要總結獨佔模式下資源的獲取。

二 資源的獲取源碼解析

在上一篇總結中,最後過源碼的時候看到addWater(),同時我們也提出兩個猜想獲取資源的兩種方式

猜想一:線程上來就直接獲取,如果獲取成功的話那就執行了,獲取失敗的話被封裝成節點添加到同步隊列中

猜想二:線程一上來看看隊列中有沒有要同步的節點,如果有的話那就不獲取資源了直接添加到同步隊列中,等待上一個節點喚醒。

現在看一下操作state的方法有哪些


    /**
     * 返回當前同步狀態
     */
    protected final int getState() {
        return state;
    }

    /**
     * 設置當前的同步狀態
     */
    protected final void setState(int newState) {
        state = newState;
    }

    /**
     * 使用CAS來更新state的值
     *
     * @param except 期望值
     * @param update 更新值
     * @return 更新是否成功
     */
    protected final boolean compareAndSetState(int except, int update) {
        return unsafe.compareAndSwapInt(this, stateOffset, except, update);
    }

AQS下面這個方法就是來獲取資源state的

    /**
     * 以獨佔模式獲取,忽略中斷。實現 至少調用一次{@link #tryAcquire},
     * 成功迴歸。 否則,線程可能會排隊      
     *
     * @param arg 資源請求
     */
    public final void acquire(int arg) {
        if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
            selfInterrupt();
        }
    }

看到上面的代碼我們看到了比較熟悉的方法就是addWaier(),這個方法返回一個封裝好線程的節點,被當作參數傳遞到acquireQueued()這個方法中。但是acquireQueued執行不執行取決與前面的tryAcquire()這個方法。當tryAcquire()返回false的時候纔會去執行acquireQueued()這個方法。再看一下tryAcquire()這個方法。

    protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }

盡然拋出一個異常,在第一遍的文章說過AQS是基於模板方法的框架,既然使用的是模板方法那就需要子類去實現了,在ReentrantLock內部類Sync中是AQS的實現。下面是源碼

  @Override
        protected final boolean tryAcquire(int acquire) {
            return nonfairTryAcquire(acquire);
        }

 final boolean nonfairTryAcquire(int acquires) {
            //獲取但當前線程
            final Thread current = Thread.currentThread();
            //獲取資源的狀態
            int c = getState();
            if (c == 0) {
                //如果資源的狀態爲0的話說明state是沒有線程持有當前資源的
                if (compareAndSetState(0, acquires)) {
                    //使用CAS替換,如果成功那就將獨佔線程設爲當前線程,也就意味着當前線程
                    //擁有執行時間了
                    setExclusiveOwnerThread(current);
                     //獲取的資源返回true
                    return true;
                }
            } else if (current == getExclusiveOwnerThread()) {
                //如果state不爲0的話獨佔線程是當前線程的話那麼給state加一,這裏是
                //重入鎖的實現
                int nextc = c + acquires;
                if (nextc < 0) {
                    throw new Error("Maximum lock count exceeded");
                }
                setState(nextc);
                return true;
            }
            //如果沒獲取到資源的話返回false
            return false;
        }

好了tryAcquire()方法幹什麼的已經知道了,如果獲取到資源的話返回true,沒有獲取到資源的話就返回false,現在再看acquire()這個方法,當線程獲取到資源的時候返回的是true ,!true也就是false,那就不必在在執行&&面的語句了,如果沒有獲取到資源,就要執行後面的語句了,首先將當前線程包裝成一個節點添加到對列尾部並返回這個節點。既然包裝了,那就要處理將這個節點了。acquireQueued方法就是幹這個的,下面是源碼

 /**
     * 將競爭節點設置爲頭節點,同時當前節點不是頭節點的話
     *
     * @param node 要獲取頭節點的節點
     * @param arg  state狀態參數
     * @return 返回true表示線程發生了中斷
     */
    final boolean acquireQueued(final Node node, int arg) {
        //這個變量來看是否要取消節點的競爭
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (; ; ) {
                //獲取node的前驅節點
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {//當前的前驅節點是頭節點,那麼當前節點就獲取資源
                    //將node設置爲頭節點
                    setHead(node);
                    //消除引用有利於垃圾回收
                    p.next = null;
                    failed = false;
                    return interrupted;
                }
                //這裏是for循環的移動條件跳過前驅接節點未取消狀態的節點,
                //當前線程中斷的話只能返回去等待了
                if (shouldParkAfterFailedAcquire(p, node) &&
                        parkAndCheckInterrupt()) {
                    interrupted = true;
                }
            }
        } finally {
            //當線程中斷直接取消當前節點競爭
            if (failed) {
                cancelAcquire(node);
            }
        }

    }

對於上面一段源碼來說,獲取資源是不難理解的,但是沒有獲取到資源時候執行了一個if語句,看一下if語句中兩個方法中分別做了什麼。下面是源碼


    /**
     * 檢查是否可以在節點後添加競爭節點,同時檢查node前驅節點是否取消,如果取消了就要將這個節點
     * 移除掉,如果在前驅節點等待中返回true
     *
     * @param pred 前驅節點
     * @param node 當前節點
     * @return 返回boolean
     */
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        //獲取前驅節點的狀態
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL) {
            return true;
        }
        if (ws > 0) {
            do {
                //如果前驅節點的的狀態爲取消狀態那麼跳過直到找到可以
                //獲取競爭的node
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

當node中的前驅節點是等待中的時候就會執行下一個方法,下一個方法的源碼如下

    /**
     * 將線程至於waiting中 並返回線程是否中斷
     *
     * @return 中斷線程的boolean
     */
    private final boolean parkAndCheckInterrupt() {
        //將線程至於waiting中
        LockSupport.park(this);
        return Thread.interrupted();
    }

看到這裏基本上已經清楚了,acquireQueued()方法就是如果可以獲取到資源的時候直接獲取,不能獲取到資源時檢查父節點是否在等待狀態中,如果在等待中,就調用LockSupport.park(),將當前線程至於等待狀態,等待中斷或着喚醒。AQS獲取資源也就完了。

AQS獲取資源的總結:

1.首先先使用CAS獲取支援state。如果獲取成功的話就不在添加節點了,如果獲取失敗的話將當前線程封裝到node中。

2.在添加節點的時候判斷前驅節點是否是頭節點,如果前驅節點是頭節點的話,繼續for循環獲取資源。如果不是頭節點的話檢查當前的前驅節點是否爲空,將當前節點的所有前面的節點爲取消狀態的全都去掉。去掉之後當前節點還不是頭節點時,將線程至於waiting狀態等待中斷或喚醒。

上面的總結也就驗證了的猜想一,其實就是非公平鎖的實現,猜想二是公平的鎖的實現。可以在ReentrantLock中的內部類FairSync看到公平鎖的實現。

三 資源的釋放

關於支援的釋放我認爲是比較簡單的,可以大體的猜想一下,找到持有資源的node節點,將state的值設置爲0,再將當前node節點從同步隊列中移除掉。然後喚醒下一個節點的線程。下面是源碼

    /**
     * 釋放資源,並喚醒下一個節點
     *
     * @param arg 狀態
     * @return 釋放成功
     */
    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0) {
                //喚醒下一個節點
                unparkSuccessor(h);
            }
            return true;
        }
        return false;
    }

上面源碼幹什麼已經在註釋中說的很清楚了,但是沒有看到資源是如何操作的。其中資源的操作就在tryRelease()方法中下面是源碼。

    /**
     * 線程調用釋放鎖
     *
     * @param arg 狀態
     * @return 是否釋放成功
     */
    protected boolean tryRelease(int arg) {
        throw new UnsupportedOperationException();
    }

這個方法和tryAcquire()方法一樣都是拋了一個異常我們看其中子類的實現。下面是源碼。

    /**
         * 釋放資源
         *
         * @param release 釋放鎖的數字
         * @return 是否釋放成功
         */
        @Override
        protected final boolean tryRelease(int release) {
            //獲取到資源並減去資源
            int c = getState() - release;
            
            if (Thread.currentThread() != getExclusiveOwnerThread()) {
                throw new IllegalMonitorStateException();
            }
            boolean free = false;
            if (c == 0) {
                free = true;
                //當state爲0的時候表示資源釋放完成想獨佔的線程設置爲null
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

四 總結

 從上面的源碼中我們可以看出,鎖的實現無非就是資源的獲取,與隊列的操作,線程狀態的轉化。

獲取資源時:先操作state,操作成功就直接獲取到了,操作不成功添加到同步隊列中,調用LockSuppport.park(),將線程至於waiting狀態等待中斷或喚醒。

資源釋放:先操作state,操作成功的話將隊列中的當前節點移除,喚醒下一個節點。

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