聊一聊JUC同步隊列AQS


看了這篇文章下次有人在問你AQS你可以跟他好好扯一扯了,喜歡請點贊關注,多謝鴨

1. AQS是什麼?

AbstractQueuedSynchronizer抽象同步隊列簡稱AQS,AQS是一個FIFO的雙向隊列,通過同步隊列來完成對同步狀態的管理。它是JUC實現同步器d的重要基礎抽象類。
我們先看看AQS及lock類關係圖:
類關係圖

2. AQS實現原理

AQS通過內部的同步隊列(一個FIFO雙向隊列)來完成同步狀態的管理,當前線程獲取同步狀態失敗時,同步器會將當前線程以及等待狀態等信息構造成爲一個節點(Node)並將其加入同步隊列,同時會阻塞當前線程,當同
步狀態釋放時,會把首節點中的線程喚醒,使其再次嘗試獲取同步狀態。
下面看看AQS類圖:
AQS
如圖所示:AQS是一個抽象類,它裏面有兩個內部類Node和COnditionObject。還有一個子類Sync,Sync他是在ReentrantLock中的內部抽象類。
AQS的使用依靠繼承來完成,子類通過繼承自AQS並實
現所需的方法來管理同步狀態。例如常見的ReentrantLock,CountDownLatch等AQS的兩種功能從使用上來說,AQS的功能可以分爲兩種:獨佔和共享。

  • 獨佔鎖模式下,每次只能有一個線程持有鎖,比如ReentrantLock就是以獨佔方式實現的互斥鎖
  • 共享鎖模式下,允許多個線程同時獲取鎖,併發訪問共享資源,比如ReentrantReadWriteLock。
    很顯然,獨佔鎖是一種悲觀保守的加鎖策略,它限制了讀/讀衝突,如果某個只讀線程獲取鎖,則其他讀線程都只能等待,這種情況下就限制了不必要的併發性,因爲讀操作並不會影響數據的一致性。共享鎖則是一種樂觀鎖,它
    放寬了加鎖策略,允許多個執行讀操作的線程同時訪問共享資源

2.1 Node節點

2.1.1 Node是AQS中的靜態內部類。

主要屬性如下:

static final class Node { 
	//標記線程是獲取共享資源時被阻塞掛起後放入AQS隊列的
	static final Node SHARED = new Node();
	//標記線程是獲取獨佔資源時被掛起後放入AQS隊列的
	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;
	/**
	* 表示節點是等待狀態,包含cancelled(取消),SIGNAL(線程需要被喚醒)
	* CONDITION(線程在等待condition 也就是在condition隊列中)			
	* 
	*/
	int waitStatus; 
	Node prev; //前繼節點 
	Node next; //後繼節點 
	Node nextWaiter; //存儲在condition隊列中的後繼節點 
	Thread thread; //當前線程 
}

AQS類底層的數據結構是使用雙向鏈表,是隊列的一種實現。包括一個head節點和一個tail節點,分別表示頭結點和尾節點,其中頭結點不存儲Thread,僅保存next結點的引用。

2.1.2 入隊操作

我們看看如何將一個線程包裝成一個節點放入隊列中。

    private Node enq(final Node node) {
    	//無意義的死循環,操作成功則返回
        for (;;) {
        	//(1)
            Node t = tail;
            if (t == null) { // Must initialize
            	//(2)
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
            	//(3)
                node.prev = t;
                if (compareAndSetTail(t, node)) {	//(4)
                    t.next = node;	(5)
                    return t;
                }
            }
        }
    }
  • (1)第一次進來,t==null
  • (2)通過unsafe的CAS設置哨兵節點爲頭節點,如果設置成功,將爲尾節點也指向頭節點。
  • (3)設置前驅節點爲尾部節點
  • (4)然後將當前node設置到尾部
  • (5)然後將尾部節點的後驅節點設爲node,這樣新添加的node節點就是尾部節點了。

2.1.3 CAS操作

在上面提到了調用unsafe類執行CAS操作
unsafe類中有很多compareAndSetXXX方法。這個是一個native方法,比如上面調用了unsafe#compareAndSwapObject方法

public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
  • 第一個參數爲需要改變的對象;
  • 第二個爲偏移量(即相對於需要改變的對象的首地址的偏移值,
  • 第三個參數爲期待的值,
  • 第四個爲更新後的值
    整個方法的作用是如果當前時刻的值等於預期值var4相等,則更新爲新的期望值 var5,如果更新成功,則返回true,否則返回false。

AQS中,除了本身的鏈表結構以外,還有一個很關鍵的功能,就是CAS,這個是保證在多線程併發的情況下保證線程安全的前提下去把線程加入到AQS中的方法,可以簡單理解爲樂觀鎖。首先,用到了unsafe類,(Unsafe類是在sun.misc包下,不屬於Java標準。但是很多Java的基礎類庫,包括一些被
廣泛使用的高性能開發庫都是基於Unsafe類開發的,比如Netty、Hadoop、Kafka等;Unsafe可認爲是Java中留下的後門,提供了一些低層次操作,如直接內存訪問、線程調度等)。

說到這裏就知道了unsafe類是有多叼了吧。
如果理解了上面,我們在看看剛剛AQS中設置頭節點,請保持耐心繼續往下看,小夥伴們,要深入啊…

public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
//內部類Node    
static final class Node {
	//(1)頭節點
	private transient volatile Node head;
	private transient volatile Node tail;
	private volatile int state;
	//其他省略
}
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long headOffset;
	//注意看這裏!!!!!,
    static {
        try {
        	//(2)獲取屬性相對於類對象首地址的偏移值
        	stateOffset = unsafe.objectFieldOffset
   (AbstractQueuedSynchronizer.class.getDeclaredField("state"));
            headOffset = unsafe.objectFieldOffset
   (AbstractQueuedSynchronizer.class.getDeclaredField("head"));
            tailOffset = unsafe.objectFieldOffset
   (AbstractQueuedSynchronizer.class.getDeclaredField("tail"));
            waitStatusOffset = unsafe.objectFieldOffset
                (Node.class.getDeclaredField("waitStatus"));
            nextOffset = unsafe.objectFieldOffset
                (Node.class.getDeclaredField("next"));
        } catch (Exception ex) { throw new Error(ex); }
    }
	//設置頭節點
	private final boolean compareAndSetHead(Node update) {
		//(2)
        return unsafe.compareAndSwapObject(this, headOffset, null, update);
    }
}    

講解:

  • (1)在內部類Node定義了屬性head,
  • (2)獲取屬性head相對於類對象首地址的偏移值headOffset
  • (3)設置頭節點調用compareAndSwapObject方法,傳入headOffset參數
  • 最後通過CAS設置node到head處。
    深入之後大家感覺怎麼樣,明白了吧!嘿嘿
    關於unsafe類下次可以拓展寫一篇文章。

2.2 AQS同步隊列

下面以獨佔方式講解下鎖的獲取與釋放,默認就是獨佔鎖。

2.2.1 嘗試獲取鎖

#acquire嘗試獲取鎖,方法很短,但調用了很多其他方法,你細品吧

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))//(1)
            selfInterrupt();
    }
    //根據公平/非公平實現有差別,子類實現
    protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }
    private Node addWaiter(Node mode) {
     	//將當前線程創建一個獨佔的Node節點,mode爲獨佔模式
        Node node = new Node(Thread.currentThread(), mode);
		//tail是AQS的中表示同步隊列隊尾的屬性,剛開始爲null,所以進行enq(node)方法
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            //防止有其他線程修改tail,使用CAS進行修改,如果失敗則降級至full enq
            if (compareAndSetTail(pred, node)) {
            	//// 如果成功之後舊的tail的next指針再指向新的tail,成爲雙向鏈表
                pred.next = node;
                return node;
            }
        }
        //文章前面已講解
        enq(node);
        return node;
    }

#acquire方法作用

  • tryAcquire 首先通過cas去修改state的狀態,如果修改成功表示競爭鎖成功,如果失敗的,tryAcquire會返回false,
  • Node.EXCLUSIVE 表示node節點爲獨佔的,addWaiter方法把當前線程封裝成Node,並添加到隊列的尾部

aqs

addWaiter返回了插入的節點,作爲acquireQueued方法的入參,這個方法主要用於爭搶鎖

	final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
            	// 獲取prev節點,若爲null即刻拋出 NullPointException
                final Node p = node.predecessor();
                //如果前驅爲head纔有資格進行鎖的搶奪,所以判斷頭部節點
                if (p == head && tryAcquire(arg)) {
                // 獲取鎖成功後就不需要再進行同步操作了,獲取鎖成功的線程作爲新的 head節點
                    setHead(node);
                    //head節點,head.thread與head.prev爲null, 但是head.next不爲null
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //如果獲取鎖失敗,則根據節點的waitStatus決定是否需要掛起線程
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())// 若前面爲true,則執行掛起,待下次喚醒的時候檢測中斷的標誌	
                    interrupted = true;
            }
        } finally {
        // 如果拋出異常則取消鎖的獲取,進行出隊(sync queue)操作
            if (failed)
                cancelAcquire(node);
        }
    }
    // predecessor 獲取前驅節點
	final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
   }

獲取鎖成功時,頭節點出隊列,如下圖:
獲取鎖成功

2.2.2 鎖的釋放

這個動作可以認爲就是一個設置鎖狀態的操作,而且是將狀態減掉傳入的參數值(參數是1),如果結果狀態爲0,就將排它鎖的Owner設置爲null,以使得其它的線程有機會進行執行。 在排它鎖中,加鎖的時候狀態會增加1(當
然可以自己修改這個值),在解鎖的時候減掉1,同一個鎖,在可以重入後,可能會被疊加爲2、3、4這些值,只有unlock()的次數與lock()的次數對應纔會將Owner線程設置爲空,而且也只有這種情況下才會返回true。

protected final boolean tryRelease(int releases) {
            int c = getState() - releases;// 這裏是將鎖的數量減1
            if (Thread.currentThread() != getExclusiveOwnerThread())// 如果釋放的線程和獲取鎖的線程 不是同一個,拋出非法監視器狀態異常
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {// 直到最後一次釋放鎖時,纔會把當前線程釋放
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

2.3 ConditionObject

AQS另一個內部類是ConditionObject,用來結合鎖實現線程同步。它可以訪問AQS對象內部的變量,它是一個條件變量,可以讓線程達到某個條件時才能被喚醒。每個條件變量對應一個條件隊列。條件隊列是一個單鏈表的隊列,用來存放線程調用await方法後被阻塞的線程。

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