03-J.U.C之AQS

1. AQS 簡介

        AQS(AbstractQueuedSynchronizer),即隊列同步器。它是構建鎖或者其他同步組件的基礎框架(如ReentrantLockReentrantReadWriteLockSemaphore等),它是JUC併發包中的核心基礎組件。

        AQS解決了實現同步器時涉及到的大量細節問題,例如獲取同步狀態、FIFO同步隊列。基於AQS來構建同步器可以帶來很多好處。它不僅能夠極大地減少實現工作,而且也不必處理在多個位置上發生的競爭問題。在基於AQS構建的同步器中,只能在一個時刻發生阻塞,從而降低上下文切換的開銷,提高了吞吐量。同時在設計AQS時充分考慮了可伸縮行,因此J.U.C中所有基於AQS構建的同步器均可以獲得這個優勢。

        AQS使用一個int類型的成員變量 state來表示同步狀態,當 state > 0時表示已經獲取了鎖,當state = 0時表示釋放了鎖。它提供了三個方法 getState()setState(int newState)compareAndSetState(int expect, int update)來對同步狀態state進行操作,當然AQS可以確保對state的操作是線程安全的

        AQS主要提供瞭如下一些方法:

  • getState():返回同步狀態的當前值;
  • setState(int newState):設置當前同步狀態;
  • compareAndSetState(int expect, int update):使用CAS設置當前狀態,該方法能夠保證狀態設置的原子性;
  • tryAcquire(int arg):獨佔式獲取同步狀態,獲取同步狀態成功後,其他線程需要等待該線程釋放同步狀態才能獲取同步狀態;
  • tryRelease(int arg):獨佔式釋放同步狀態;
  • tryAcquireShared(int arg):共享式獲取同步狀態,返回值大於等於0則表示獲取成功,否則獲取失敗;
  • tryReleaseShared(int arg):共享式釋放同步狀態;
  • isHeldExclusively():當前同步器是否在獨佔式模式下被線程佔用,一般該方法表示是否被當前線程所獨佔;
  • acquire(int arg):獨佔式獲取同步狀態,如果當前線程獲取同步狀態成功,則由該方法返回,否則,將會進入同步隊列等待,該方法將會調用可重寫的tryAcquire(int arg)方法;
  • acquireInterruptibly(int arg):與acquire(int arg)相同,但是該方法響應中斷,當前線程爲獲取到同步狀態而進入到同步隊列中,如果當前線程被中斷,則該方法會拋出InterruptedException異常並返回;
  • tryAcquireNanos(int arg,long nanos):超時獲取同步狀態,如果當前線程在nanos時間內沒有獲取到同步狀態,那麼將會返回false,已經獲取則返回true
  • acquireShared(int arg):共享式獲取同步狀態,如果當前線程未獲取到同步狀態,將會進入同步隊列等待,與獨佔式的主要區別是在同一時刻可以有多個線程獲取到同步狀態;
  • acquireSharedInterruptibly(int arg):共享式獲取同步狀態,響應中斷;
  • tryAcquireSharedNanos(int arg, long nanosTimeout):共享式獲取同步狀態,增加超時限制;
  • release(int arg):獨佔式釋放同步狀態,該方法會在釋放同步狀態之後,將同步隊列中第一個節點包含的線程喚醒;
  • releaseShared(int arg):共享式釋放同步狀態;

        AQS通過內置的FIFO同步隊列來完成資源獲取線程的排隊工作,如果當前線程獲取同步狀態失敗(鎖)時,AQS則會將當前線程以及等待狀態等信息構造成一個節點(Node)並將其加入同步隊列,同時會阻塞當前線程,當同步狀態釋放時,則會把節點中的線程喚醒,使其再次嘗試獲取同步狀態

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {

	// 指向同步隊列的頭節點
	private transient volatile Node head;

	// 指向同步隊列的尾節點
    private transient volatile Node tail;

	// 同步狀態
	private volatile int state;

	protected final int getState() {
     	return state;
 	}

	protected final void setState(int newState) {
      	state = newState;
  	}

	protected final boolean compareAndSetState(int expect, int update) {
      	return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
 	}
}

在這裏插入圖片描述
        Node中的 thread 變量用來存放進入 AQS 隊列裏面的線程;

        Node 節點內部的 SHARD 用來標記該線程是獲取共享資源時被阻塞掛起後放入 AQS 隊列的。

        EXCLUSIVE 用來標記線程是獲取獨佔資源時被掛起後放入 AQS 隊列的;

        waitStatus 記錄當前線程等待狀態,可以爲 CANCELLED(線程被取消了)、SIGNAL(線程需要被喚醒)、CONDITION(線程在條件隊列裏面等待)、PROPAGATE(釋放共享資源時需要通知其他節點);

        prev 記錄當前節點的前驅節點,next 記錄當前節點的後繼節點。

2. CLH 同步隊列

        AQS內部維護着一個FIFO隊列,該隊列就是CLH同步隊列。CLH同步隊列是一個FIFO雙向隊列,AQS依賴它來完成同步狀態的管理,當前線程如果獲取同步狀態失敗時,AQS則會將當前線程已經等待狀態等信息構造成一個節點(Node)並將其加入到CLH同步隊列,同時會阻塞當前線程,當同步狀態釋放時,會把首節點喚醒(公平鎖),使其再次嘗試獲取同步狀態。在CLH同步隊列中,一個節點表示一個線程,它保存着線程的引用(thread)、狀態(waitStatus)、前驅節點(prev)、後繼節點(next),其定義如下:

static final class Node {

		// 鎖分爲兩種鎖,一種是共享鎖,另一種是獨佔鎖
        /** 標記獲取共享鎖失敗,在隊列中等待的節點 */
        static final Node SHARED = new Node();
        
        /** 標記獲取獨佔鎖失敗,在隊列中等待的節點 */
        static final Node EXCLUSIVE = null;
        

		// 以下是 waitStatus 的值,用來表示當前節點中現成的狀態
        /** 
         *	表示當前結點已取消調度。當timeout或被中斷(響應中斷的情況下),會觸發變更爲此狀態,進入該狀態後的結點將不會再變化。
     	 */
        static final int CANCELLED =  1;

        /** 
         *	表示後繼結點在等待當前結點喚醒。後繼結點入隊時,會將前繼結點的狀態更新爲SIGNAL。
     	 */
        static final int SIGNAL    = -1;


        /**
    	 * 表示結點等待在Condition上,當其他線程調用了Condition的signal()方法後,
    	 * CONDITION狀態的結點將從等待隊列轉移到同步隊列中,等待獲取同步鎖。
     	 */
        static final int CONDITION = -2;
        
        /**
     	 * 共享模式下,前繼結點不僅會喚醒其後繼結點,同時也可能會喚醒後繼的後繼結點。
         */
        static final int PROPAGATE = -3;

       /**
    	* 節點狀態,包含上面四種狀態(另外還有一種初始化,新結點入隊時的默認狀態。)
     	* 特別注意:它是volatile關鍵字修飾的,保證對其線程可見性,但是不保證原子性。
     	* 所以更新狀態時,採用CAS方式去更新, 如:compareAndSetWaitStatus
     	*/
        volatile int waitStatus;

        /**
     	 * 前驅節點,比如當前節點被取消,那就需要前驅節點和後繼節點來完成連接。
     	 */
        volatile Node prev;

        /**
     	 * 後繼節點。
     	 */
        volatile Node next;

       /**
     	* 入隊列時的當前線程。
     	*/
        volatile Thread thread;

        /**
     	 * 存儲condition隊列中的後繼節點。
     	 */
        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() {    // Used to establish initial head or SHARED marker
        }

        Node(Thread thread, Node mode) {     // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
        }

        Node(Thread thread, int waitStatus) { // Used by Condition
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

在這裏插入圖片描述
在這裏插入圖片描述

2.1 入隊

        當一個線程獲取了同步狀態(或者鎖),其他線程將無法獲取到同步狀態,轉而被構造成爲節點加入到同步隊列中,而這個加入隊列的過程必須要保證線程安全,同步器使用CAS的方式,將節點加入到同步隊列的尾節點

private Node addWaiter(Node mode) {
    //以給定模式構造結點。mode有兩種:EXCLUSIVE(獨佔)和SHARED(共享)
    Node node = new Node(Thread.currentThread(), mode);
    //快速嘗試添加尾節點
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        //CAS設置尾節點
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    //多次嘗試
    enq(node);
    return node;
}

        addWaiter(Node node)先通過快速嘗試設置尾節點,如果失敗,則調用enq(Node node)方法設置尾節點。

private Node enq(final Node node) {
    //多次嘗試,直到成功爲止
    for (;;) {
        Node t = tail;
        //tail不存在,設置爲首節點
        if (t == null) {
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            //設置爲尾節點
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

        在上面代碼中,兩個方法都是通過一個CAS方法compareAndSetTail(Node expect, Node update)來設置尾節點,該方法可以確保節點是線程安全添加的。在enq(Node node)方法中,AQS通過“死循環”的方式來保證節點可以正確添加,只有成功添加後,當前線程纔會從該方法返回,否則會一直執行下去。

        同步器將節點加入到同步隊列的過程如下:

在這裏插入圖片描述

2.2 出隊

        CLH同步隊列遵循FIFO首節點的線程釋放同步狀態後,將會喚醒它的後繼節點(也就是說是鏈表中的第二個節點,並不是首節點),而後繼節點將會在獲取同步狀態成功時將自己設置爲首節點,這個過程非常簡單,head執行該節點並斷開原首節點的next和當前節點的prev即可,注意在這個過程是不需要使用CAS來保證的,因爲只有一個線程能夠成功獲取到同步狀態。過程圖如下:

private void setHead(Node node) {
    head = node;
    node.thread = null;
    node.prev = null;
}

在這裏插入圖片描述        設置首節點是通過獲取同步狀態成功的線程來完成的,由於只有一個線程能夠成功獲取到同步狀態,因此設置頭節點的方法並不需要使用CAS來保證,它只需要將首節點設置成爲原首點的後繼節點並斷開首結點的next引用即可

3. 同步狀態的獲取和釋放

3.1 獨佔式

        獨佔式,同一時刻僅有一個線程持有同步狀態。

3.1.1 同步狀態獲取

        acquire(int arg)方法爲AQS提供的模板方法,該方法爲獨佔式獲取同步狀態,但是該方法對中斷不敏感,也就是說由於線程獲取同步狀態失敗加入到CLH同步隊列中,後續對線程進行中斷操作時,線程不會從同步隊列中移除。代碼如下:

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

        函數流程如下:

         1. tryAcquire:去嘗試獲取鎖,獲取成功則設置鎖狀態並返回true,否則返回false。該方法自定義同步組件自己實現,該方法必須要保證線程安全的獲取同步狀態。
         2. addWaiter:將該線程加入等待隊列的尾部,並標記爲獨佔模式;
         3. acquireQueued:使線程阻塞在等待隊列中獲取資源,一直獲取到資源後才返回。如果在整個等待過程中被中斷過,則返回true,否則返回false
         4. selfInterrupt:如果線程在等待過程中被中斷過,它是不響應的。只是獲取資源後纔再進行自我中斷selfInterrupt(),將中斷補上。

        acquireQueued方法爲一個自旋的過程,也就是說當前線程(Node)進入同步隊列後,就會進入一個自旋的過程,每個節點都會自省地觀察,當條件滿足,獲取到同步狀態後,就可以從這個自旋過程中退出,否則會一直執行下去。如下:

	// node 爲獲取鎖失敗,並已經被加入到同步隊列中的節點
    final boolean acquireQueued(final Node node, int arg) {
    	//標記是否成功拿到資源
        boolean failed = true;
        try {
        
        	//標記等待過程中是否被中斷過
            boolean interrupted = false;
            
            // 自旋
            for (;;) {
            	// 獲取當前節點的前驅節點
            	// 如果前驅是head,即該結點已成老二,那麼便有資格去嘗試獲取資源(可能是老大釋放完資源喚醒自己的,當然也可能被interrupt了)。
                final Node p = node.predecessor();
                // 如果前驅節點是頭節點,則當前節點嘗試去獲取鎖
                // 爲什麼這樣?
                // 頭節點是成功獲取到同步狀態的節點,而頭節點的線程釋放了同步狀態之後,將會喚醒其後繼節點,             
                // 後繼節點的線程被喚醒後需要檢查自己的前驅節點是否是頭節點
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    // 返回等待過程中是否被中斷過
                    return interrupted;
                }
				// 執行到這裏說明存在競爭,有多個線程都在等待一個鎖
                
				// 如果當前線程獲取鎖失敗或者前驅節點不是頭節點,則執行下面的方法

				// shouldParkAfterFailedAcquire 方法的作用有三個:
				// 1、如果這個節點已經取消了獲取鎖的操作,則刪除隊列中所有被取消了的節點。
				// 2、若狀態位不爲Node.SIGNAL,且沒有取消操作,則會嘗試將狀態位修改爲Node.SIGNAL
				// 3、狀態位是Node.SIGNAL,表明線程是否已經準備好被阻塞並等待喚醒。
				// 最終,只有在pred.waitStatus已經等於Node.SIGNAL時纔會返回true。
            	// 其他情況返回false,然後acquireQueued會繼續循環。

				// 在shouldParkAfterFailedAcquire返回true之後,
           		// acquireQueued方法體內繼續執行parkAndCheckInterrupt():
                // 該方法調用LockSupport.park()方法使線程阻塞。
                // 這裏阻塞的原因是由於輪不到其獲取鎖,避免無效的自旋,浪費CPU資源
                if (shouldParkAfterFailedAcquire(p, node) &&
                
                    parkAndCheckInterrupt())	//裏面會對當前線程執行中斷,當被喚醒時,繼續循環
                    
                    // 如果等待過程中被中斷過,哪怕只有那麼一次,就將interrupted標記爲true
                    interrupted = true;
            }
        } finally {
            if (failed)
            	// 如果等待過程中沒有成功獲取資源(如timeout,或者可中斷的情況下被中斷了),那麼取消結點在隊列中的等待。
                cancelAcquire(node);
        }
    }

        acquire(int arg)方法流程圖如下:
在這裏插入圖片描述

3.1.2 獲取響應中斷

        AQS提供了acquire(int arg)方法以供獨佔式獲取同步狀態,但是該方法對中斷不響應,對線程進行中斷操作後,該線程會依然位於CLH同步隊列中等待着獲取同步狀態。爲了響應中斷,AQS提供了acquireInterruptibly(int arg)方法,該方法在等待獲取同步狀態時,如果當前線程被中斷了,會立刻響應中斷拋出異常InterruptedException

public final void acquireInterruptibly(int arg)throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    // 當前線程獲取同步狀態失敗後
    if (!tryAcquire(arg))
        doAcquireInterruptibly(arg);
}

        首先校驗該線程是否已經中斷了,如果是則拋出InterruptedException,否則執行tryAcquire(int arg)方法獲取同步狀態,如果獲取成功,則直接返回,否則執行doAcquireInterruptibly(int arg)

        doAcquireInterruptibly(int arg)定義如下:

private void doAcquireInterruptibly(int arg) throws InterruptedException {
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

        doAcquireInterruptibly(int arg)方法與acquire(int arg)方法僅有兩個差別:

        1.方法聲明拋出InterruptedException異常

        2.在中斷方法處不再是使用interrupted標誌,而是直接拋出InterruptedException異常。

3.1.3 超時獲取

        AQS除了提供上面兩個方法外,還提供了一個增強版的方法:tryAcquireNanos(int arg,long nanos)。該方法爲acquireInterruptibly方法的進一步增強,它除了響應中斷外,還有超時控制。即如果當前線程沒有在指定時間內獲取同步狀態,則會返回false,否則返回true。如下:

public final boolean tryAcquireNanos(int arg, long nanosTimeout)
    throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    return tryAcquire(arg) ||
        doAcquireNanos(arg, nanosTimeout);
}

        tryAcquireNanos(int arg, long nanosTimeout)方法超時獲取最終是在doAcquireNanos(int arg, long nanosTimeout)中實現的,如下:

private boolean doAcquireNanos(int arg, long nanosTimeout)
    throws InterruptedException {
    //nanosTimeout <= 0
    if (nanosTimeout <= 0L)
        return false;
    //超時時間
    final long deadline = System.nanoTime() + nanosTimeout;
    //新增Node節點
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        //自旋
        for (;;) {
            final Node p = node.predecessor();
            //獲取同步狀態成功
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return true;
            }
            /*
             * 獲取失敗,做超時、中斷判斷
             */
            //重新計算需要休眠的時間
            nanosTimeout = deadline - System.nanoTime();
            //已經超時,返回false
            if (nanosTimeout <= 0L)
                return false;
            //如果沒有超時,則等待nanosTimeout納秒
            //注:該線程會直接從LockSupport.parkNanos中返回,
            //LockSupport爲JUC提供的一個阻塞和喚醒的工具類,後面做詳細介紹
            if (shouldParkAfterFailedAcquire(p, node) &&
                nanosTimeout > spinForTimeoutThreshold)
                LockSupport.parkNanos(this, nanosTimeout);
            //線程是否已經中斷了
            if (Thread.interrupted())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

        針對超時控制,程序首先記錄喚醒時間deadline ,deadline = System.nanoTime() + nanosTimeout(時間間隔)。如果獲取同步狀態失敗,則需要計算出需要休眠的時間間隔nanosTimeout(= deadline – System.nanoTime()),如果nanosTimeout <= 0表示已經超時了,返回false,如果大於spinForTimeoutThreshold(1000L)則需要休眠nanosTimeout ,如果nanosTimeout <=spinForTimeoutThreshold ,就不需要休眠了,直接進入快速自旋的過程。原因在於 spinForTimeoutThreshold 已經非常小了,非常短的時間等待無法做到十分精確,如果這時再次進行超時等待,相反會讓nanosTimeout的超時從整體上面表現得不是那麼精確,所以在超時非常短的場景中,AQS會進行無條件的快速自旋。

在這裏插入圖片描述

3.1.4 線程阻塞

        在線程獲取同步狀態時如果獲取失敗,則加入CLH同步隊列,通過通過自旋的方式不斷獲取同步狀態,但是在自旋的過程中則需要判斷當前線程是否需要阻塞,其主要方法在acquireQueued()

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    	
    	// 獲取前驅節點的 waitStatus
        int ws = pred.waitStatus;
        
        if (ws == Node.SIGNAL)
            // 已經告訴前驅節點,讓其拿到鎖後通知自己
            return true;
        if (ws > 0) {
            /*
             * 前驅節點被取消,跳過前驅節點
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            
            //如果前驅正常,那就把前驅的狀態設置成SIGNAL,告訴它拿到鎖後通知自己一下。
            // 有可能失敗,人家說不定剛剛釋放完呢!
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

        這段代碼主要檢查當前線程是否需要被阻塞,具體規則如下:

        1、如果當前線程的前驅節點狀態爲SINNAL,則表明其前驅節點需要被喚醒,當前線程需要被阻塞,直接返回true,讓當前線程阻塞。

        2、如果當前線程的前驅節點狀態爲CANCELLED(ws > 0),則表明該線程的前驅節點已經等待超時或者被中斷了,則需要從CLH隊列中將該前驅節點刪除掉,直到回溯到前驅節點狀態 <= 0,返回false

        3、如果前驅節點非SINNAL,非CANCELLED,則通過CAS的方式將其前驅節點設置爲SINNAL,返回false

        整個流程中,如果前驅結點的狀態不是SIGNAL,那麼自己就不能安心去休息,需要去找個安心的休息點,同時可以再嘗試下看有沒有機會輪到自己獲取鎖

        如果 shouldParkAfterFailedAcquire(Node pred, Node node) 方法返回true,則調用parkAndCheckInterrupt()方法阻塞當前線程:

	// 讓當前線程阻塞
    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

        parkAndCheckInterrupt()方法主要是把當前線程掛起,從而阻塞住線程的調用棧,同時返回當前線程的中斷狀態。其內部則是調用LockSupport工具類的park()方法來阻塞該方法。

在這裏插入圖片描述

3.1.5 同步狀態釋放

        當線程獲取同步狀態後,執行完相應邏輯後就需要釋放同步狀態。AQS提供了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;
}
protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    // 如果當前線程不是鎖的擁有者
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    // 如果鎖的重入數爲0,則將鎖的擁有者置爲null
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    // 更新 state 的值
    setState(c);
    return free;
}
    private void unparkSuccessor(Node node) {
        
         //這裏,node一般爲當前線程所在的結點。         
        int ws = node.waitStatus;
        if (ws < 0)
        	// 置零當前線程所在的結點狀態,允許失敗。
            compareAndSetWaitStatus(node, ws, 0);

        
        //找到下一個需要喚醒的結點s
        Node s = node.next;
        //如果爲空或已取消
        if (s == null || s.waitStatus > 0) {
        	//如果爲空或已取消
            s = null;
            // 從後向前找,找到一個可以被喚醒的節點
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
        	// 喚醒線程
            LockSupport.unpark(s.thread);
    }

        release方法同樣是先調用自定義同步器自定義的tryRelease(int arg)方法來釋放同步狀態,釋放成功後,會調用unparkSuccessor(Node node)方法喚醒後繼節點,unparkSuccessor(Node node)方法使用LockSupport來喚醒處於等待狀態的線程

        在AQS中維護着一個FIFO的同步隊列,當線程獲取同步狀態失敗後,則會加入到這個CLH同步隊列的隊尾並一直保持着自旋(自旋到一定程度後,會進入阻塞)。在CLH同步隊列中的線程在自旋時會判斷其前驅節點是否爲首節點,如果爲首節點則不斷嘗試獲取同步狀態,獲取成功則退出CLH同步隊列。當線程執行完邏輯後,會釋放同步狀態,釋放後會喚醒其後繼節點。

3.2 共享式

        共享式與獨佔式的最主要區別在於同一時刻獨佔式只能有一個線程獲取同步狀態,而共享式在同一時刻可以有多個線程獲取同步狀態。例如讀操作可以有多個線程同時進行,而寫操作同一時刻只能有一個線程進行寫操作,其他操作都會被阻塞。

3.2.1 獲取同步狀態

public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        //獲取失敗,自旋獲取同步狀態
        doAcquireShared(arg);
}

        這裏tryAcquireShared()依然需要自定義同步器去實現。但是AQS已經把其返回值的語義定義好了:負值代表獲取失敗;0代表獲取成功,但沒有剩餘資源;正數表示獲取成功,還有剩餘資源,其他線程還可以去獲取。所以這裏acquireShared()的流程就是:

         1、tryAcquireShared()嘗試獲取資源,成功則直接返回;
         2、失敗則通過doAcquireShared()進入等待隊列,直到獲取到資源爲止才返回。

private void doAcquireShared(int arg) {
    // 共享式節點
    // 把當前線程封裝到一個SHARE類型Node中,添加到SyncQueue末尾上
    final Node node = addWaiter(Node.SHARED);
    
    // 是否成功標誌
    boolean failed = true;
    try {
        
        // 等待過程中是否被中斷過的標誌
        boolean interrupted = false;
        for (;;) {
            //前驅節點
            final Node p = node.predecessor();
            // 如果前繼節點是head節點,獲取同步狀態
            if (p == head) {
                
                //嘗試獲取同步
                int r = tryAcquireShared(arg);
                
                // 返回值大於等於0,表示該次獲取同步狀態成功並從自旋過程中退出
                if (r >= 0) {
                    
                    // 將head指向自己,還有剩餘資源可以再喚醒之後的線程
                    setHeadAndPropagate(node, r);
                    // head已經指向node節點,oldHead.next索引置空,方便p節點對象回收
                    p.next = null; // help GC
                    
                    // 如果等待過程中被打斷過,此時將中斷補上。
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            
            // 判斷狀態,尋找安全點,進入waiting狀態,等着被unpark()或interrupt()
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

         跟獨佔模式比,還有一點需要注意的是,如果有剩餘的話還會喚醒之後的隊友。那麼問題就來了,假如老大用完後釋放了5個資源,而老二需要6個,老三需要1個,老四需要2個。老大先喚醒老二,老二一看資源不夠,他是把資源讓給老三呢,還是不讓?答案是否定的!老二會繼續park()等待其他線程釋放資源,也更不會去喚醒老三和老四了。獨佔模式,同一時刻只有一個線程去執行,這樣做未嘗不可;但共享模式下,多個線程是可以同時執行的,現在因爲老二的資源需求量大,而把後面量小的老三和老四也都卡住了。當然,這並不是問題,只是AQS保證嚴格按照入隊順序喚醒罷了(保證公平,但降低了併發)。

private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; 
    setHead(node);//head指向自己
     //如果還有剩餘量,繼續喚醒下一個鄰居線程
    if (propagate > 0 || h == null || h.waitStatus < 0) {
        Node s = node.next;
        if (s == null || s.isShared())
            doReleaseShared();
    }
}

3.2.2 釋放同步狀態

public final boolean releaseShared(int arg) {
	//嘗試釋放資源
    if (tryReleaseShared(arg)) {
    	//喚醒後繼結點
        doReleaseShared();
        return true;
    }
    return false;
}

         因爲可能會存在多個線程同時進行釋放同步狀態資源,所以需要確保同步狀態安全地成功釋放,一般都是通過CAS和循環來完成的。

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