二刷Java多線程:Java併發包中鎖詳解(一):抽象同步隊列AQS

前言

在之前學習Java多線程的過程中,我對於Java併發包中鎖相關實現原理這塊知識一直是一知半解。在這次二刷Java多線程的過程中,對這塊的知識慢慢有了自己的理解,個人認爲學習這塊內容的學習路線很重要,下面來分享一下我個人認爲比較正確的學習路線,如果你也在爲這塊內容發愁,希望對你有一點啓發

Java併發包中鎖的實現離不開抽象同步隊列AbstractQueuedSynchronizer,也就是常說的AQS,而AQS的實現依賴於Unsafe中的CAS相關的方法和volatile關鍵字,所以在學AQS之前,不光要對多線程的很多基礎知識有個瞭解,而且要了解魔法類Unsafe以及CAS相關知識,這裏可以參考之前的博客。而在學習AQS的過程中也要對AQS中提供的模板方法和子類需要重寫的方法有個大致的瞭解,這樣在看獨佔鎖和讀寫鎖的源碼的過程中才能理清楚思路,哪些方法是直接使用AQS中的模板方法,哪些方法是子類重寫的。如果是第一次學習這塊知識,建議先從實現原理上去理解,再去分析實現細節。這次二刷多線程的過程中,學習了《Java併發編程之美》、《Java併發編程的藝術》、極客時間的《Java併發編程實戰》以及網上很多優秀的博客,這次綜合這些資料對知識點進行了整理,希望你看完會有所收穫

一、抽象同步隊列AQS

隊列同步器AbstractQueuedSynchronizer是用來構建鎖或者其他組件的基本框架,它使用了一個int成員變量表示同步狀態,通過內置的FIFO隊列來完成資源獲取線程的排隊工作

同步器的主要使用方式是繼承,子類通過繼承同步器並實現它的抽象方法來管理同步狀態,在抽象方法的實現過程中免不了要對同步狀態進行更改,這時就需要使用同步器提供的3個方法來進行操作,因爲它們能夠保證狀態的改變是安全的。子類推薦被定義爲自定義同步組件的靜態內部類

同步器是實現鎖的關鍵,在鎖的實現中聚合同步器,利用同步器實現鎖的語義。可以這樣理解二者之間的關係:鎖是面向使用者的,它定義了使用者與鎖交互的接口,隱藏了實現細節;同步器面向的是鎖的實現者,它簡化了鎖的實現方式,屏蔽了同步狀態管理、線程的排隊、等待與喚醒等底層操作

1、隊列同步器的接口

同步器的設計是基於模板方法模式的,使用者需要繼承同步器並重寫指定的方法,隨後將同步器組合在自定義同步組件的實現中,並調用同步器提供的模板方法,而這些模板方法將會調用使用者重寫的方法

同步器提供的功能可以分爲獨佔功能和共享功能兩類

1)、重寫同步器的指定方法時,需要使用同步器提供的如下3個方法來訪問或修改同步狀態:

	//同步狀態共享變量,使用volatile修飾保證線程可見性
	private volatile int state;

	//獲取當前同步狀態
	protected final int getState() {
        return state;
    }

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

	//使用CAS設置當前狀態,該方法能保證狀態設置的原子性
    protected final boolean compareAndSetState(int expect, int update) {
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }

2)、同步器中可重寫的方法如下:

	//獨佔模式:
	//獨佔式獲取同步狀態,實現該方法需要查詢當前狀態並判斷同步狀態是否符合預期,然後再進行CAS設置同步狀態
    protected boolean tryAcquire(int arg)
   	//獨佔式釋放同步狀態,等待獲取同步狀態的線程將有機會獲取同步狀態
    protected boolean tryRelease(int arg)
    
    //共享模式:
    //共享式獲取同步狀態,返回大於等於0的值,表示獲取成功,反之,獲取失敗
    protected int tryAcquireShared(int arg)
    //共享式釋放同步狀態
    protected boolean tryReleaseShared(int arg)
        
    //當前同步器是否在獨佔模式下被線程佔用,一般該方法表示是否被當前線程所獨佔
    protected boolean isHeldExclusively()        

3)、同步器提供的模板方法基本上分爲3類:獨佔式獲取與釋放同步狀態、共享式獲取與釋放同步狀態和查詢同步隊列中的等待線程情況,具體如下:

	//獨佔式獲取同步狀態,如果當前線程獲取同步狀態成功,則由該方法返回,否則,將會進入同步隊列等待,該方法將會調用重寫的tryAcquire(int arg)
	public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

	//與acquire(int arg)相同,但是該方法響應中斷,當前線程未獲取到同步狀態而進入同步隊列中,如果當前線程被中斷,則該方法會拋出InterruptedException並返回
    public final void acquireInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (!tryAcquire(arg))
            doAcquireInterruptibly(arg);
    }

	//在acquireInterruptibly(int arg)基礎上增加了超時限制,如果當前線程在超時時間內沒有獲取到同步狀態,那麼將會返回false,如果獲取到了返回true
    public final boolean tryAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        return tryAcquire(arg) ||
            doAcquireNanos(arg, nanosTimeout);
    }

	//共享式的獲取同步狀態,如果當前線程未獲取到同步狀態,將會進入同步隊列等待,與獨佔模式的主要區別是在同一時刻可以有多個線程獲取到同步狀態
    public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

	//與acquireShared(int arg)相同,該方法響應中斷
    public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }

	//在acquireSharedInterruptibly(int arg)基礎上增加了超時限制
    public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        return tryAcquireShared(arg) >= 0 ||
            doAcquireSharedNanos(arg, nanosTimeout);
    }

	//獨佔式的釋放同步狀態,該方法會在釋放同步狀態之後,將同步隊列中第一個節點包含的線程喚醒
    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

	//共享式的釋放同步狀態
    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

	//獲取等待在同步隊列上的線程集合	
    public final Collection<Thread> getQueuedThreads() {
        ArrayList<Thread> list = new ArrayList<Thread>();
        for (Node p = tail; p != null; p = p.prev) {
            Thread t = p.thread;
            if (t != null)
                list.add(t);
        }
        return list;
    }

2、同步隊列

同步器依賴內部的同步隊列(一個FIFO雙向隊列)來完成同步狀態的管理,當前線程獲取同步狀態失敗時,同步器會將當前線程以及等待狀態等信息構造成爲一個節點並將其加入同步隊列,同時會阻塞當前線程,當同步狀態釋放時,會把首節點中的線程喚醒,使其再次嘗試獲取同步狀態

同步隊列中的節點(Node結點類)用來保存獲取同步狀態失敗的線程引用、等待狀態以及前驅和後繼節點

   static final class Node {
       	//標記一個正在共享模式中等待的節點
        static final Node SHARED = new Node();
       
        //標記一個正在獨佔模式中等待的節點
        static final Node EXCLUSIVE = null;       
       
        static final int CANCELLED =  1;

        static final int SIGNAL    = -1;

        static final int CONDITION = -2;

        static final int PROPAGATE = -3;
       
        volatile int waitStatus;

       	//前驅結點,當節點加入同步隊列時被設置(尾部添加)
        volatile Node prev;

       	//後繼節點
        volatile Node next;

       	//獲取同步狀態的線程
        volatile Thread thread;

       	//連接到下個等待狀態的結點,或者特殊值SHARED。因爲條件隊列只有在獨佔模式中保持時纔會被訪問,所以只需要一個簡單的隊列來在節點等待Condition時保持節點。然後,被轉移到隊列重新獲取。因爲Condition只能是排他的,所以使用特殊值來指示共享模式
        Node nextWaiter;

waitStatus用來表示等待狀態,包含如下狀態:

  • CANCELLED:值爲1,由於在同步隊列中等待的線程等待超時或者被中斷,需要從同步隊列中取消等待,節點進入該狀態將不會變化
  • SIGNAL:值爲-1,後繼節點的線程處於等待狀態,而當前節點的線程如果釋放呃同步狀態或者被取消,將會通知後繼節點,使後繼節點的線程得以運行
  • CONDITION:值爲-2,節點在等待隊列中,節點線程等待在Condition上,當其他線程對Condition調用了signal()方法後,該節點將會從等待隊列中轉移到同步隊列中,加入到對同步狀態的獲取中
  • PROPAGATE:值爲-3,表示下一次共享式同步狀態獲取將會無條件地被傳播下去
  • INITIAL:值爲0,初始狀態

節點是構成同步隊列的基礎,同步器擁有首節點和尾節點,沒有成功獲取同步狀態的線程將會成爲節點加入該隊列的尾部
在這裏插入圖片描述
同步器包含了兩個節點類型的引用,一個指向頭節點,而另一個指向尾節點。當一個線程成功地獲取了同步狀態(或者鎖),其他線程將無法獲取到同步狀態,轉而被構造稱爲節點並加入到同步隊列中,而這個加入隊列的過程必須要保證線程安全,因此同步器提供了一個基於CAS的設置尾節點的方法:compareAndSetTail(Node expect, Node update),它需要傳遞當前線程認爲的尾節點和當前節點,只有設置成功後,當前節點才正式與之前的尾節點建立關聯
在這裏插入圖片描述
同步隊列遵循FIFO,首節點是獲取同步狀態成功的節點,首節點的線程在釋放同步狀態時,將會喚醒後繼節點,而後繼節點將會在獲取同步狀態成功時將自己設置爲首節點
在這裏插入圖片描述
設置首節點是通過獲取同步狀態成功的線程來完成的,由於只有一個線程能夠成功獲取到同步狀態,因此設置頭節點的方法並不需要使用CAS來保證,它只需要將首節點設置成爲原首節點的後繼節點並斷開原首節點的next引用即可

3、獨佔式同步狀態獲取與釋放

通過調用同步器的acquire(int arg)方法可以獲取同步狀態,該方法對中斷不敏感,也就是由於線程獲取同步狀態失敗後進入同步隊列中,後繼對線程進行中斷操作時,線程不會從同步隊列中移出

	//獨佔模式獲取鎖的方法,排隊狀態時可能多次阻塞和非阻塞。通常用來實現lock方法
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

首先調用自定義同步器實現的tryAcquire(int arg)方法,該方法保證線程安全的獲取同步狀態,如果同步狀態獲取失敗,則構造同步節點並通過addWaiter(Node mode)方法將該節點加入到同步隊列的尾部,最後調用acquireQueued(final Node node, int arg)方法,使得該節點以死循環的方式獲取同步狀態。如果獲取不到則阻塞節點中的線程,而被阻塞線程的喚醒主要依靠前驅結點的出隊或阻塞線程被中斷來實現

    private Node addWaiter(Node mode) {
        //創建一個獨佔式結點
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        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;
            //如果隊尾結點是空,初始化head和tail(懶加載),這裏是通過CAS設置尾結點,不成功就一直重試
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } 
            //如果尾結點非空,就採用CAS操作將當前結點插入到尾結點後面,如果在插入的時候尾結點有變化,就將尾結點向後移動直到移動到最後一個結點爲止,然後再把當前結點插入到尾結點後面,尾指針指向當前結點,入隊成功
            else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

最後,看下acquireQueued方法:

    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; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

acquireQueued內部也是一個死循環,只有前驅結點是頭結點的結點,纔有機會去tryAcquire。若tryAcquire成功,表示獲取同步狀態成功,就將此結點設置爲頭結點;若前驅結點不是頭結點,或者tryAcquire失敗,則進入shouldParkAfterFailedAcquire去判斷判斷當前線程是否應該阻塞,若可以,調用parkAndCheckInterrupt阻塞當前線程,直到被中斷或者被前驅結點喚醒。若還不能阻塞,繼續循環
在這裏插入圖片描述
獨佔式同步狀態獲取流程:
在這裏插入圖片描述
當前線程獲取同步狀態並執行了相應邏輯之後,就需要釋放同步狀態,使得後續節點能夠繼續獲取同步狀態。通過調用同步器的release(int arg)方法可以釋放同步狀態,該方法在釋放了同步狀態之後,會喚醒其後繼節點(進而使後繼節點重新嘗試獲取同步狀態)

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

該方法執行時,會喚醒頭節點的後繼節點線程,unparkSuccessor(Node node)方法使用LockSupport來喚醒處於等待狀態的線程

獨佔式同步狀態獲取和釋放過程:在獲取同步狀態時,同步器維護一個同步隊列,獲取狀態失敗的線程都會被加入到隊列中並在隊列中進行自旋;移出隊列(或停止自旋)的條件是前驅結點爲頭節點且成功獲取了同步狀態。在釋放同步狀態時,同步器調用tryRelease(int arg)方法釋放同步狀態,然後喚醒頭節點的後繼節點

4、共享式同步狀態獲取與釋放

對於共享式同步組件來講,同一時刻可以有多個線程同時獲取到同步狀態。嘗試獲取同步狀態的方法tryAcquireShared返回值爲int

    protected int tryAcquireShared(int arg) {
        throw new UnsupportedOperationException();
    }

返回值情況如下:

  • 當返回值大於0時,表示獲取同步狀態成功,同時還有剩餘同步狀態可供其他線程獲取

  • 當返回值等於0時,表示獲取同步狀態成功,但沒有可用同步狀態了

  • 當返回值小於0時,表示獲取同步狀態失敗

獲取同步狀態:

    public final void acquireShared(int arg) {
         //返回值小於0,獲取同步狀態失敗,需要進行排隊
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
		 //獲取同步狀態成功,直接返回去幹自己的事兒        
    }

   private void doAcquireShared(int arg) {
       	//構造一個共享結點,添加到同步隊列尾部
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            //線程parking過程中是否被中斷過
            boolean interrupted = false;
            for (;;) {
                //找到前驅結點
                final Node p = node.predecessor();
                //頭結點持有同步狀態,只有前驅是頭結點,纔有機會嘗試獲取同步狀態
                if (p == head) {
                    //嘗試獲取同步裝填
                    int r = tryAcquireShared(arg);
                    //r>=0,獲取成功,r 值表示剩餘可用同步狀態
                    if (r >= 0) {
                        //獲取成功就將當前結點設置爲頭結點,若還有可用資源,傳播下去,也就是繼續喚醒後繼結點
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                //是否能進入parking狀態
                if (shouldParkAfterFailedAcquire(p, node) &&
                    //阻塞線程
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

大體邏輯與獨佔式的acquireQueued差距不大,只不過由於是共享式,會有多個線程同時獲取到線程,也可能同時釋放線程,空出很多同步狀態,所以前驅結點是頭結點的結點獲取到同步狀態,如果還有可用資源,會繼續傳播下去

釋放同步狀態:

    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

    private void doReleaseShared() {
        //死循環,共享模式,持有同步狀態的線程可能有多個,採用循環CAS保證線程安全
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    //喚醒後繼結點
                    unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }

該方法在釋放同步狀態之後,將會喚醒後續處於等待狀態的節點。對於能夠支持多個線程同時訪問的併發組件,它和獨佔式主要區別在於tryReleaseShared(int arg)方法必須確保同步狀態線程安全釋放,一般是通過循環和CAS來保證的,因爲釋放同步狀態的操作會同時來自多個線程

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