AbstractQueuedSynchronizer深度學習(獨佔鎖)

概念

AbstractQueuedSynchronizer簡稱AQS,即抽象隊列同步器。AQS是JAVA併發編程的核心基礎類。所有的併發都是基於AQS進行擴展。AQS內部採用FIFO隊列進行線程併發的管理。
類AQS採用模板方法進行設計,可以讓更多的子類去擴展自己需要的功能。AQS主要提供兩種模式併發模型:
1. 獨佔模式:任何時刻只能有一個線程獲得當前執行鎖定
2. 共享模式:任何時刻可以有多個線程獲得當前執行鎖定
本文主要講述獨佔模式的鎖定。

狀態同步

AQS主要是通過如下3個方法進行狀態同步,獲取鎖成功狀態設置爲1,釋放鎖狀態設置爲0:
1. getState()
2. setState(int expert)
3. compareAndSetState(int expert,int update)

模板方法

  1. acquire(int arg)
    1.1 該方法是獨佔模式下獲取同步狀態,如果獲取鎖成功,則直接返回。如果獲取鎖失敗,則進入同步隊列中,同時調用子類(NonFairSync和FairSync)中重寫的tryAcquire方法。後續詳細說明
  2. acquireInterruptliby
    2.1 該方法與acquire相似,但是該方法響應線程中斷。如果線程進入同步隊列中被打斷,則該方法拋出異常。
  3. tryAcquireNanos
    3.1 該方法只是增加了超時限制,在規定 的時間內如果線程未獲得同步狀態,則返回false。獲得同步狀態則返回true。
  4. release()
    4.1 該方法爲釋放同步狀態,待當前線程釋放同步狀態後,會將其後繼節點中的線程喚醒。該方法參數1,最後調用LockSupport的unpark方法完成喚醒首節點等待的線程

重寫方法

  1. tryAcquire
    1.1 需要子類去重寫,在ReentrantLock中,具體的子類FairSync和NonFairSync重寫tryAcquire方法。實現該方法需要查詢當前狀態並判斷是否符合預期值,調用CAS進行設置。
  2. tryRelease
    2.1 該方法是獨佔模式下的釋放同步狀態。釋放同步狀態後會將後繼節點中的線程喚醒。
  3. isHeldExclusively()
    3.1 該方法是監測當前鎖定是否爲當前線程

同步隊列

  1. AQS(同步器)中的同步隊列是基於FIFO設計,是一個雙向鏈表。當線程獲取同步狀態失敗後,會將當前線程,當前等待狀態,前驅節點,後驅節點等信息封裝成一個節點Node。加入到阻塞隊列(FIFO)。
  2. 阻塞隊列中的每一個節點都是封裝了獲取當前同步狀態失敗的線程引用,等待狀態,前驅節點引用,後繼節點引用。
    同步隊列的結構圖如下:
    這裏寫圖片描述
  3. 根據上圖可以看出,同步管理器內部其實就是一個雙向鏈表。其中每一個節點本身都持有前驅節點後驅節點的引用,當前阻塞線程的引用當前線程在隊列中的狀態。同時隊列的尾節點就是tail節點。
  4. 在AQS中,同步隊列採用靜態內部類方式實現,通過源碼分析一下數據結構:
 static final class Node {
        //共享模式
        static final Node SHARED = new Node();
        // 獨佔模式
        static final Node EXCLUSIVE = null;
        /***線程狀態waitStatus的各種狀態值***/
        // 狀態爲1,狀態取消,從FIFO去取消
        static final int CANCELLED =  1;
        // 表明當前節點的後繼節點需要被喚醒,
        static final int SIGNAL    = -1;
        // 表明該節點的線程處於等待隊列中,不在同步隊列中。
        static final int CONDITION = -2;
        // 當前下一次共享模式下會將同步狀態傳播下去
        static final int PROPAGATE = -3;
        /**waitStauts=1:表明該線程由於超時或者被中斷,需要從同步隊列中取消。該節    
        **              點狀態將不會再次發生變化。
        **waitStauts=-1:表明當前節點的後繼節點(lockSupport的park方法阻塞)需要
        **              被喚醒,實際通過LockSupport的unpark喚醒。如果當前節點
        **              釋放同步狀態或被取消(狀態爲1),需要通知它的後繼節點
        **waitStauts=2: 表明當前節點處於等待隊列中,不是同步隊列中。節點的線程等
        **              待在Condition上,需要調用Condition的singal方法將這個
        **              節點從等待隊列轉入到同步隊列,同時狀態設置爲0?(不確定)             
        **waitStauts=3:表明下一次共享模式下將會傳播該狀態
        **waitStauts=0:初始狀態,線程什麼也不做。
        ** waitStatus狀態值的改變都是通過CAS操作
        **/
        volatile int waitStatus;
        // 當前節點的引用prev指向前驅節點
        volatile Node prev;
        // 當前節點的引用next指向後繼節點
        volatile Node next;
        // 當前節點持有的阻塞線程,FIFO中每個節點都是封裝了一個阻塞線程
        volatile Thread thread;
        // 等待隊列中的後繼節點,需要調用Condition才能將其轉入到同步隊列中
        Node nextWaiter;

        /**
         * Returns true if node is waiting in shared mode
         */
        final boolean isShared() {
            return nextWaiter == SHARED;
        }
       /**
       **  返回當前節點的前驅節點,代碼顯而易見。
       **/
        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }
       // Used to establish initial head or SHARED marker
        Node() {  }
         // 線程阻塞時,加入新的對尾節點需要調用該構造方法
         // thread:當前阻塞線程
        Node(Thread thread, Node mode) {    
            this.nextWaiter = mode;
            this.thread = thread;
        }
         // Used by Condition
        Node(Thread thread, int waitStatus) {
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

添加結點

  1. 併發環境中發生獲取鎖等待時,JVM底層就會將當前阻塞的線程加入到同步隊列中。再構造同步隊列時,需要首先實例化head和tail節點。添加節點過程如下:
    • 添加節點1:第一次添加節點時,需要構造同步隊列,前提就是要初始化head節點和tail節點。初始化head和tail節點時,二者指向同一個空節點(不持有當前阻塞線程的引用)。加入第一個節點時,將head.next=node1;node1.prev=head。同時,將tail引用指向node1,tail= node1,即tail指向node1。
    • 添加節點2:將node2加入到node1的尾部。設置node1.next=node2;node2.prev=node1。同時將node2設置成尾節點。
    • 添加節點3:步驟如同添加節點2。
      這裏寫圖片描述

acquire()

  1. 在AQS中acquire就是線程在獨佔模式下獲取鎖的方式,也是ReentrantLock中加鎖lock方法中調用的底層方法。
  2. acquire方法調用tryAcquire方法實現獲取鎖,即上述子類需要實現的tryAcquire方法。如果tryAcquire方法獲取成功,則線程獲得執行鎖。否則,當前線程進入同步隊列阻塞,直到獲取鎖定或者被中斷。
  3. 分析一下acquire源碼:
 /** 如果tryAcquire成功(arg=1),則該線程獲得執行鎖。
 ** 如果tryAcquire失敗,addWaiter方法是構造一個Node然後加入到FIFO隊列尾部
 ** acquireQueued方法是指從同步隊列中的結點都以自旋的形式獲取同步狀態,獲取到同
 ** 步狀態的結點,將自身設置爲頭結點。
 **/
 public final void acquire(int arg) {
     if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            // accquireQueued成功後,重置status爲true
            selfInterrupt();
    }

tryAcquire

  1. 獨佔鎖模式下tryAcquire方法是由NonFairSync實現,本身又調用了NonFairSync的方法nonfairTryAcquire。這個方法是功能很簡單。主要有2部分構成。
    1.1. 獲取鎖成功,調用compareAndSetState (0,1)非阻塞的併發方法。成功之後,設置當前 鎖的擁有者爲當前線程:setExclusiveOwnerThread(currentThread)。
    1.2. 第二部分是,ReentrantLock實現可重入鎖的實現。
 final boolean nonfairTryAcquire(int acquires) {
           // 當前線程對象current
            final Thread current = Thread.currentThread();
           // 阻塞線程初始狀態都爲0
            int c = getState();
            if (c == 0) {
                // 設置獲取同步狀態,成功設置爲1,
                if (compareAndSetState(0, acquires)) {
                    // 設置當前線程擁有鎖的權力
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            // 下面是爲ReentrantLock的支持重入鎖設計
            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;
        }

addWaiter

  1. 這個方法是創建節點Node並且將結點加入同步隊列尾部。線程在獲取鎖阻塞時,調用此方法將當前線程對象,等待狀態等信息封裝成Node對象,插入到同步隊列的尾部
  2. 分析addWaiter源碼
 private Node addWaiter(Node mode) {
       // 將當前執行線程以獨佔模式構造成Node對象,mode是獨佔模式。
       // node作爲尾結點加入
        Node node = new Node(Thread.currentThread(), mode);
        //pred!=null,判斷尾節點指向引用是否爲空,即是否爲空隊列,第一次構建同步
        //隊列時是爲空。如果爲空,直接執行enq方法。
        Node pred = tail;
        if (pred != null) {
           // 新加入結點node的前驅結點指向原來的尾結點
            node.prev = pred;
            //調用同步方法重新設置尾結點,當前節點即是尾節點tail。
            // tail引用指向node尾節點
            if (compareAndSetTail(pred, node)) {
            // 原尾結點的next指向新的尾結點
                pred.next = node;
                return node;
            }
        }
        // 首次構建隊列時,纔會執行該方法
        enq(node);
        return node;
    }

enq

該方法以自旋的形式實現。當第一次構建隊列時,執行該方法。

/**
** 構造同步隊列,初始化head,tail值。
**/
 private Node enq(final Node node) {
        for (;;) {// 以自旋形式運行,直到滿足條件才調出結束運行
            Node t = tail;
            // 第一次加入阻塞的結點時,必須初始化head和tail值。
            if (t == null) {
            // 設置頭結點,線程安全併發操作。只會有一個線程成功。
            // head和tail指向一個空節點,head和tail此時相同,並且沒有持有當前
            // 阻塞線程的引用以及狀態。 
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
            // 自旋一次之後,head和tail節點不爲空,將node結點加入到鏈表中
            // 當前結點的前驅結點爲頭結點。
                node.prev = t;
            // 設置尾結點,將當前節點node設置爲尾節點。
                if (compareAndSetTail(t, node)) {
             // tail指向當前尾結點,即tail就是node節點。
             // 最終head.next=node,node.prev=head。tail=node。
                    t.next = node;
                    return t;
                }
            }
        }
    }

acquireQueued

  1. 該方法展示了在併發環境中,所有線程都在同步隊列中以自旋的形式等待判斷自己當前結點是否是頭結點。只有currentNode的前驅結點(prevNode)是頭結點(headNode)時,該currentNode纔有可能獲得同步狀態。即currentNode獲得同步鎖時,需要將自己本身設置爲頭節點。可以這樣理解:頭結點的含義就是相當於獲得執行的鎖
  2. shouldParkAfterFailedAcquire將阻塞的線程狀態由0設置爲-1和檢查curentNode的前驅結點狀態,如果是1,則丟棄該節點。
  3. parkAndCheckInterrupt當前阻塞線程調用LockSupport.park方法進行阻塞,等待喚醒(響應中斷或者獲得鎖定)
  4. 該方法的流程如下:這裏寫圖片描述
  5. 方法源碼如下,分析:
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)) {
                 // 設置頭結點,釋放當前線程。其他無用屬性設置爲Null,進行GC
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                /** 
                ** shoundParkAfterFailedAcquire將同步隊列中的節點
                ** 狀態由0設置爲-1和檢查當前節點的前驅結點狀態。
                ** parkAndCheckInterrupt將狀態爲-1的節點進行阻塞,調用
                **LockSupport.park方法完成,等待喚醒。
                **/
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

shouldParkAfterFailedAcquire

  1. 該方法主要判斷currentNode的前驅結點prevNode的狀態。根據prevNode狀態進行後續操作。
    1.1 prevNode的狀態爲-1,則執行parkAndCheckInterrupt方法中斷當前線程
    1.2 prevNode狀態爲1,即大於0.則直接忽略該結點。從同步隊列中取消。
    1.3 其他狀態(新加入節點是0,其他沒有),調用compareAndSetWaitStatus設置prevNode狀態爲-1
  2. 該方法功能流程如下:
    這裏寫圖片描述
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
      //當前結點的前驅結點的狀態,無論前驅結點是否是頭結點
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL) // 狀態爲-1,prevNode釋放同步狀態後,會通         
            return true;       //知其後繼結點
        if (ws > 0) {// 值大於0,即爲1,已取消狀態,則前驅結點直接從隊列中刪除
            do {     // 當前結點的前驅結點直接指向前驅結點的前驅結點,即跳過
               // currentNode的前驅結點。原前驅結點引用指向新的prevNode
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            //currentNode的新前驅結點引用指向當前結點
            pred.next = node;
        } else {
           //嘗試設置prevNode的狀態爲-1,Node.SIGNAL
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

parkAndCheckInterrupt

  1. 同步隊列中的節點進行阻塞,等待喚醒。
 private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

併發同步方法

在AQS中,多個線程進行併發操作而又不阻塞的操作是通過CAS機制進行保證的。AQS中封裝了Unsafe的很多併發操作方法。
1. compareAndSetHead(compareAndSetHead)方法,設置頭結點

 /**
     * CAS head field. Used only by enq.
     */
    private final boolean compareAndSetHead(Node update) {
        return unsafe.compareAndSwapObject(this, headOffset, null, update);
    }
  1. compareAndSetTail(Node expect, Node update),設置尾結點
 /**
     * CAS tail field. Used only by enq.
     */
    private final boolean compareAndSetTail(Node expect, Node update) {
        return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
    }
  1. compareAndSetWaitStatus(Node node,int expect, int update)設置等待狀態
/**
     * CAS waitStatus field of a node.
     */
    private static final boolean compareAndSetWaitStatus(Node node,
                                                         int expect,
                                                         int update) {
        return unsafe.compareAndSwapInt(node, waitStatusOffset,
                                        expect, update);
    }
  1. compareAndSetNext(Node node,Node expert,Node update)設置當前節點後繼節點
 /**
     * CAS next field of a node.
     */
    private static final boolean compareAndSetNext(Node node,
                                                   Node expect,
                                                   Node update) {
        return unsafe.compareAndSwapObject(node, nextOffset, expect, update);
    }

未完待續

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