【java併發】AQS中acquire方法解析

       AQS,全名AbstractQueuedSynchronizer(抽象隊列同步器),它是CLH(不明白的可以先了解一下CLH)的變種。它與CLH不同之處在於:
       CLH是一種公平鎖,它是通過自旋同步隊列中節點的前驅結點狀態,判斷同步隊列中的節點是否能夠進入臨界區;AQS的同步隊列中的節點不會以自旋的方式來進入臨界區,而是先以公平或者不公平的方式嘗試進入臨界區,如果不能,則進行阻塞,等待被喚醒,再去嘗試是否能夠進入臨界區。


       AQS屬於隊列,那麼就是一個一個節點連接而成,在AQS中節點的數據結構如下:

static final class Node {
      
        static final Node SHARED = new Node();
      
        /** 獨佔鎖模式 */
        static final Node EXCLUSIVE = null;
        
        /**
         * AQS中判斷節點是否爲取消狀態,有時候是判斷狀態值是否大於零
         */
        static final int CANCELLED =  1;
      
        /** 
         * 如果節點是這個狀態,
         * 那麼該節點就需要喚醒它的後繼節點 
         */
        static final int SIGNAL    = -1;
        
        /**
         * 不明白,但是不影響對acquire方法的理解
         */
        static final int CONDITION = -2;
        
         /**
         * 不明白,但是不影響對acquire方法的理解
         */
        static final int PROPAGATE = -3;
		
		/** 
		 * 值爲:-3 , -2 , -1 , 0 , 1 
		 */
        volatile int waitStatus;

        volatile Node prev;

        volatile Node next;
		
        volatile Thread thread;
        
        Node nextWaiter;
        
        final boolean isShared() {
            return nextWaiter == SHARED;
        }
        
        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }

        Node() {
        }

        Node(Thread thread, Node mode) { 
            this.nextWaiter = mode;
            this.thread = thread;
        }

        Node(Thread thread, int waitStatus) {
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

可以看出來AQS同步隊列是一個雙向鏈表結構。需要說明的是除了頭節點是進入到臨界區的節點,其他大部分節點(有一部分節點可能被取消了,waitStatus=1)都是希望進入臨界區的節點。


acquire方法解析

       AQS中的acquire方法,是獲取獨佔鎖方法,它的代碼很簡單,代碼主體只有一個if判斷語句:

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

可以看到acquire方法實現,主要是下面這三個方法

  1. tryAcquire(arg)
  2. addWaiter(Node.EXCLUSIVE)
  3. acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

這三個方法的作用

  1. tryAcquire(arg)
    顧名思義,它就是嘗試獲取鎖,AQS在這裏沒有對其進行功能的實現,只有一個拋出異常的語句,用戶可以對其重寫實現公平鎖、不公平鎖、可重入鎖、不可重入鎖
protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
}
  1. addWaiter(Node.EXCLUSIVE)
    一旦嘗試獲取鎖未成功,就要使用該方法將其加入同步隊列尾部
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;
}

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

由於可能有多個線程併發加入隊尾產生競爭,因此,採用compareAndSetTail無鎖方法來保證同步

  1. acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
    一旦加入同步隊列,就需要使用該方法,自旋阻塞喚醒來不斷的嘗試獲取鎖,直到被中斷或獲取到鎖
final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            // 自旋
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // 幫助GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) /** 嘗試獲取鎖失敗是否阻塞線程獲取鎖 */ &&
                    parkAndCheckInterrupt() /** 阻塞 */)
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
}

前面說過,當一個節點的前驅節點的waitStatus=SIGNAL,當其前驅結點釋放鎖的時候需要對其後繼節點進行喚醒。shouldParkAfterFailedAcquire方法的功能就是判斷該節點的前驅結點的waitStatus==SIGNAL,如果相等則該節點可以阻塞,否則將該節點的前驅結點waitStatus狀態修改爲SIGNAL。由此可以知道,該節點如果沒有獲取到鎖,AQS就會盡最大努力(爲什麼說最大努力,而不是一定會將節點阻塞呢?可以思考一下,爲什麼,shouldParkAfterFailedAcquire裏面能找到答案)將該節點阻塞,之後等待前驅結點喚醒,再嘗試獲取鎖。

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