Java高頻面試之--- AQS原理解析

什麼是AQS?AQS有什麼用呢?

本篇文章主要就是解決這兩個問題,並且附上源碼解析。

AQS 的全稱是 AbstactQueuedSynchronizer 即抽象隊列同步器。

可能大部分使用Java語言的同學都知道它,因爲他是面試的高頻問題之一,面試Android也會問這樣的問題,我自己就被問了好幾次。

java併發包下很多API都是基於AQS來實現的加鎖和釋放鎖等功能的,AQS是java併發包的基礎類。比如:ReetrantLock ,ReentrantReadWriteLock 都是基於AQS來實現的。

ReentrantLock 實現加鎖和鎖釋放就是通過AQS來實現的。

先看一段代碼:

private void doTask1(){
        try {
            reentrantLock.lock();
            Log.e("aqs", "doTask1 獲得鎖");
            Thread.sleep(3 * 1000);
//            doTask2();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            reentrantLock.unlock();
            Log.e("aqs", "doTask1 釋放鎖");
        }
    }

如上,如果一個線程調用 lock 會發生什麼呢?

其實這個不難,不要一涉及到AQS就覺得很難。AQS 中維護了一個很重要的變量 state, 它是int型的,表示加鎖的狀態,初始狀態值爲0;另外 AQS 還維護了一個很重要的變量exclusiveOwnerThread,它表示的是獲得鎖的線程,也叫獨佔線程。AQS中還有一個用來存儲獲取鎖失敗線程的隊列,以及headtail 結點,包含 如下圖所示
在這裏插入圖片描述

這時,線程1 跑過來調用ReentrantLock的lock()方法嘗試進行加鎖,這個加鎖的過程,直接就是用CAS操作將state值從0變爲1。如果對CAS操作不理解的話,可以看看我之前的文章:我對CAS的理解和用法

如果這時候沒有其他的線程操作,那麼CAS操作肯定是成功的,然後設置 exclusiveOwnerThread 爲 當前線程。lock的代碼如下(這裏是以默認的非公平鎖爲例):

final void lock() {
 			//通過CAS操作 ,如果當前的state 等於0 那麼 cas 就會操作成功,返回true,表示當前線程成功獲取鎖
            if (compareAndSetState(0, 1))
                //設置exclusiveOwnerThread 爲當前線程
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

我們從ReentrantLock的名字就可以知道它是可以重入的,那麼它的重入是怎麼實現的呢?對的,就是跟 stateexclusiveOwnerThread 有關,具體是怎麼樣的呢?看下面的例子

private void doTask1(){
        try {
            reentrantLock.lock();
            Thread.sleep(3 * 1000);
            doTask2();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            reentrantLock.unlock();     
        }
    }

    private void doTask2(){
        try {
            reentrantLock.lock();         
            Thread.sleep(10 * 1000);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            reentrantLock.unlock();          
        }
    }

線程 先執行 doTask1,然後doTask1中執行doTask2,由於是同一個線程和同一把鎖,所以就可以重入了。

具體流程就是:線程執行到doTask2的時候, 執行 lock 發現 state 已經不是0而是1了,然後檢查 當前線程是不是和獲取鎖的線程是同一個,結果發現是同一個,所以 state+1 = 2,這就是可重入的核心原理。源碼如下,在 ReentranLock 中,具體的調用關係 我就不列出來了。

 final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            //如果 state = 0,那麼 通過cas來操作獲取鎖,跟之前的流程一樣,這裏爲什麼還要執行同樣的操作呢?因爲可能執行到這裏的時候,上一個線程剛好執行完,state-- 等於0
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            //這裏就是可重入的邏輯呢,
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                //小於0表示可重入的次數大於int型最大值,產生溢出了。
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

那麼 此時,如果線程2 來調用 reentrntlock.lock()方法來獲取鎖會是什麼樣子的呢?

線程2跑過來一下看到,發現state的值不是0啊?所以CAS操作將state從0變爲1的過程會失敗,因爲state的值當前爲1,說明已經有人加鎖了!

接着線程2會看一下,是不是自己之前加的鎖啊?當然不是了,exclusiveOwnerThread這個變量明確記錄了是線程1佔用了這個鎖,所以線程2此時就是加鎖失敗。

加鎖失敗後是怎麼操作的呢? 加鎖失敗後 ,此時就要將自己放入隊列中來等待,等待線程1釋放鎖之後,自己就可以重新嘗試加鎖了。

具體的代碼如下:

public final void acquire(int arg) {
// tryAcquire 就是調用上面nonfairTryAcquire,由於是線程1沒有釋放,所以線程2 調用tryAcquire返回false, 接着調用 acquireQueued方法
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

acquireQueued 中先調用 addWaiter ,addWaiter 代碼如下:

private Node addWaiter(Node mode) {
        Node node = new Node(mode);

        for (;;) {
            Node oldTail = tail;
            //表示等待隊列裏有其他的線程在等待了,然後就是設置node爲尾結點
            if (oldTail != null) {
              //當前的node的PREV 指向 尾結點oldTail 
                U.putObject(node, Node.PREV, oldTail);
               //把尾結點設置當前的node結點
                if (compareAndSetTail(oldTail, node)) {
                //之前的尾結點的next指向node
                    oldTail.next = node;
                    return node;
                }
            } else {//如果之前的等待隊列沒有等待的線程,那麼new一個node,讓head和tail指向這個new出來的結點
                initializeSyncQueue();
            }
        }
    }

上面的把node結點設置爲尾結點的操作不知道大家看明白沒?我畫個圖來說明下:
U.putObject(node, Node.PREV, oldTail);
在這裏插入圖片描述
compareAndSetTail(oldTail, node) 的操作如下,也是通過cas完成的,
在這裏插入圖片描述
oldTail.next = node; 就很簡單了,如下:
在這裏插入圖片描述
通過上面的3部操作就可以把 獲取鎖失敗的線程放到等待隊列的尾部。

接着看看 acquireQueued的源碼,如下:

final boolean acquireQueued(final Node node, int arg) {
        try {
            boolean interrupted = false;
            for (;;) {
            //判斷之前的結點是不是頭結點 head,如果是頭結點就嘗試去獲取鎖,
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                //獲取鎖成功的話,就把當前線程設置爲head
                    setHead(node);
                    //斷開之前頭結點
                    p.next = null; // help GC
                    return interrupted;
                }
                //如果之前的不是頭結點,那麼就要等待了,等候之前的線程釋放鎖後,調用 LockSupport來喚醒,
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } catch (Throwable t) {
            cancelAcquire(node);
            throw t;
        }
    }

接下來先看 shouldParkAfterFailedAcquire,如下:

 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
	    // 注意Node的waitStatus字段我們在上面創建Node的時候並沒有指定 ,默認值是0    
	    // waitStatus 的4種狀態  
	    //static final int CANCELLED =  1;    
	    //static final int SIGNAL    = -1;  //等待被喚醒  
	    //static final int CONDITION = -2;   //條件鎖使用
	    //static final int PROPAGATE = -3; //共享鎖時使用
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)           
            return true;
        // 如果 ws > 0,則表示是取消狀態,然後通過while循環 把所有是取消狀態的線程從等待隊列中刪除
        if (ws > 0) {           
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {//如果不是取消狀態,則通過cas操作將該線程的waitStatus設置爲等待喚醒狀態           
            pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
        }
        return false;
    }

上面的shouldParkAfterFailedAcquire方法只是將waitStatus設置爲SIGNAL,但是並沒有阻塞操作,真正的阻塞操作在下面的方法 parkAndCheckInterrupt,如下:

private final boolean parkAndCheckInterrupt() {
        //阻塞當前線程,底層實現是unsafe
        LockSupport.park(this);
        //返回當前線程是否被中斷
        return Thread.interrupted();
    }

這裏對lock方法作以下總結:

  1. 當線程1調用lock方法時,首先看 AQS 的 state 是否爲0,如果是0的話,通過cas操作將state置爲1,並且設置獨佔線程爲當前線程
  2. 如果這時候線程1 要調用另外一個lock方法,就像我上面的例子那樣,那麼線程1會發現 state = 1,它再去看獨佔線程是不是就是自己,如果是的話 state + 1 ,獲取鎖成功。
  3. 如果線程1 執行的方法還沒有完成即鎖還沒有釋放,此時線程2調用lock方法,由於線程1沒有釋放鎖,那麼state不會等於0,且獨佔線程是線程1而不是自己(線程2),所以AQS會把線程2放到等待隊列的尾部,如果線程2的前置結點頭結點head,那麼線程2會通過死循環一直去獲取鎖,如果不是頭結點那麼就會阻塞線程2,等待線程1釋放鎖且喚醒它。

這裏可能有點饒,看下我畫的圖。
在這裏插入圖片描述

線程鎖釋放是怎麼樣的呢?
我們知道是調用 unlock來實現的,具體是什麼樣的呢?其實很簡單 就是將 state-- 直到state = 0,然後通過 LockSupport.unpark()來喚醒等待隊列中的下一個結點。具體的看源碼:

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

這個沒啥子好說的,接着調用AQS的 release方法,如下:

public final boolean release(int arg) {
        //如果釋放當前線程成功的話,那麼就去喚醒等待隊列的頭結點
        if (tryRelease(arg)) {
            Node h = head;
            //頭結點不爲空且waitStatus不等於0
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

來看看 tryRelease 方法,實在Reentrantlock 中實現的,如下:

protected final boolean tryRelease(int releases) {
			//state - 1
            int c = getState() - releases;
            //如果當前線程不是之前設置的獨佔線程則拋出鎖狀態異常
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {//如果c == 0表示當前線程已經釋放鎖了,然後設置獨佔線程爲null,如果不等於0說明當前線程執行了可重入操作,等可重入的方法執行完 調用 unlock方法,會執行本方法 state會等於0
                free = true;
                setExclusiveOwnerThread(null);
            }
           //state 值置爲 0
            setState(c);
            return free;
        }

接下來看看 unparkSuccessor ,如下:

private void unparkSuccessor(Node node) {
        //node 表示是頭結點,如果頭結點的 waitStatue < 0,則置爲0,
        int ws = node.waitStatus;
        if (ws < 0)
            node.compareAndSetWaitStatus(ws, 0);

        //s 表示的是頭結點的下一個結點。爲什麼是喚醒下一個結點而不是頭結點呢?
        //因爲我們上面調用addWaiter方法的時候,如果等待隊列裏面沒有等待線程,那麼直接
        //new 一個Node 然後 head 和 tail 都指向這個 node,換句話說這個頭結點只是用來佔位的,所以要從頭結點的下一個結點開始喚醒
        Node s = node.next;
        //waitStatus 大於0 表示該線程已經取消了,
        if (s == null || s.waitStatus > 0) {
            s = null;
            //從隊列的尾部開始遍歷,找到一個waitStatue 小於等於0的線程來喚醒
            for (Node p = tail; p != node && p != null; p = p.prev)
                if (p.waitStatus <= 0)
                    s = p;
        }
        if (s != null)//喚醒線程
            LockSupport.unpark(s.thread);
    }

結語:AQS 結合 ReentrantLock的加鎖和解鎖已經介紹完了,有問題可以一起交流交流啊!

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