隊列同步器AbstractQueuedSynchronizer(以下簡稱同步器),是用來構建鎖或者其他同步組件的基礎框架,它使用了一個int成員變量表示同步狀態,通過內置的FIFO隊列來完成資源獲取線程的排隊工作,并發包的作者(Doug?Lea)期望它能夠成為實現大部分同步需求的基礎。
同步器的主要使用方式是繼承,子類通過繼承同步器并實現它的抽象方法來管理同步狀態,在抽象方法的實現過程中免不了要對同步狀態進行更改,這時就需要使用同步器提供的3個方法(getState()、setState(int?newState)和compareAndSetState(int?expect,int?update))來進行操作,因為它們能夠保證狀態的改變是安全的。子類推薦被定義為自定義同步組件的靜態內部類,同步器自身沒有實現任何同步接口,它僅僅是定義了若干同步狀態獲取和釋放的方法來供自定義同步組件使用,同步器既可以支持獨占式地獲取同步狀態,也可以支持共享式地獲取同步狀態,這樣就可以方便實現不同類型的同步組件(ReentrantLock、?ReentrantReadWriteLock和CountDownLatch等)。
同步器是實現鎖(也可以是任意同步組件)的關鍵,在鎖的實現中聚合同步器,利用同步器實現鎖的語義。可以這樣理解二者之間的關系:鎖是面向使用者的,它定義了使用者與鎖交互的接口(比如可以允許兩個線程并行訪問),隱藏了實現細節;同步器面向的是鎖的實現者,它簡化了鎖的實現方式,屏蔽了同步狀態管理、線程的排隊、等待與喚醒等底層操作。鎖和同步器很好地隔離了使用者和實現者所需關注的領域。
隊列同步器的接口與示例
同步器的設計是基于模板方法模式的,也就是說,使用者需要繼承同步器并重寫指定的方法,隨后將同步器組合在自定義同步組件的實現中,并調用同步器提供的模板方法,而這些模板方法將會調用使用者重寫的方法。
重寫同步器指定的方法時,需要使用同步器提供的如下3個方法來訪問或修改同步狀態。
·getState():獲取當前同步狀態。
·setState(int?newState):設置當前同步狀態。
·compareAndSetState(int?expect,int?update):使用CAS設置當前狀態,該方法能夠保證狀態設置的原子性。
同步器可重寫的方法與描述如表所示。
實現自定義同步組件時,將會調用同步器提供的模板方法,這些(部分)模板方法與描述如表所示。
同步器提供的模板方法基本上分為3類:獨占式獲取與釋放同步狀態、共享式獲取與釋放同步狀態和查詢同步隊列中的等待線程情況。自定義同步組件將使用同步器提供的模板方法?來實現自己的同步語義。
只有掌握了同步器的工作原理才能更加深入地理解并發包中其他的并發組件,所以下面通過一個獨占鎖的示例來深入了解一下同步器的工作原理。
顧名思義,獨占鎖就是在同一時刻只能有一個線程獲取到鎖,而其他獲取鎖的線程只能處于同步隊列中等待,只有獲取鎖的線程釋放了鎖,后繼的線程才能夠獲取鎖,如代碼所示。
上述示例中,獨占鎖Mutex是一個自定義同步組件,它在同一時刻只允許一個線程占有鎖。Mutex中定義了一個靜態內部類,該內部類繼承了同步器并實現了獨占式獲取和釋放同步?狀態。在tryAcquire(int?acquires)方法中,如果經過CAS設置成功(同步狀態設置為1,則代表獲取了同步狀態,而在tryRelease(int?releases)方法中只是將同步狀態重置為0。用戶使用Mutex時并不會直接和內部同步器的實現打交道,而是調用Mutex提供的方法,在Mutex的實現中,以獲取鎖的lock()方法為例,只需要在方法實現中調用同步器的模板方法acquire(int?args)即可,當前線程調用該方法獲取同步狀態失敗后會被加入到同步隊列中等待,這樣就大大降低了實現一個可靠自定義同步組件的門檻。
隊列同步器的實現分析
接下來將從實現角度分析同步器是如何完成線程同步的,主要包括:同步隊列、獨占式同步狀態獲取與釋放、共享式同步狀態獲取與釋放以及超時獲取同步狀態等同步器的核心數據結構與模板方法。
1.同步隊列
同步器依賴內部的同步隊列(一個FIFO雙向隊列)來完成同步狀態的管理,當前線程獲取同步狀態失敗時,同步器會將當前線程以及等待狀態等信息構造成為一個節點(Node)并將其加入同步隊列,同時會阻塞當前線程,當同步狀態釋放時,會把首節點中的線程喚醒,使其再次嘗試獲取同步狀態。
同步隊列中的節點(Node)用來保存獲取同步狀態失敗的線程引用、等待狀態以及前驅和后繼節點,節點的屬性類型與名稱以及描述如表所示。
節點是構成同步隊列(等待隊列)的基礎,同步器擁有首節點(head)?和尾節點(tail),沒有成功獲取同步狀態的線程將會成為節點加入該隊列的尾部,同步隊列的基本結構如圖所示。
在上圖中,同步器包含了兩個節點類型的引用,一個指向頭節點,而另一個指向尾節點。試想一下,當一個線程成功地獲取了同步狀態(或者鎖),其他線程將無法獲取到同步狀態,轉而被構造成為節點并加入到同步隊列中,而這個加入隊列的過程必須要保證線程安全,因此同步器提供了一個基于CAS的設置尾節點的方法:compareAndSetTail(Node???????expect,Node?update),它需要傳遞當前線程“認為”的尾節點和當前節點,只有設置成功后,當前節點才正式與之前的尾節點建立關聯。
同步器將節點加入到同步隊列的過程如圖所示。
同步隊列遵循FIFO,首節點是獲取同步狀態成功的節點,首節點的線程在釋放同步狀態時,將會喚醒后繼節點,而后繼節點將會在獲取同步狀態成功時將自己設置為首節點,該過程如圖所示。
在上圖中,設置首節點是通過獲取同步狀態成功的線程來完成的,由于只有一個線程能夠成功獲取到同步狀態,因此設置頭節點的方法并不需要使用CAS來保證,它只需要將首節點設置成為原首節點的后繼節點并斷開原首節點的next引用即可。
獨占式同步狀態獲取與釋放
通過調用同步器的acquire(int?arg)方法可以獲取同步狀態,該方法對中斷不敏感,也就是由于線程獲取同步狀態失敗后進入同步隊列中,后續對線程進行中斷操作時,線程不會從同步隊列中移出,該方法代碼如下所示。
上述代碼主要完成了同步狀態獲取、節點構造、加入同步隊列以及在同步隊列中自旋等待的相關工作,其主要邏輯是:首先調用自定義同步器實現的tryAcquire(int??arg)方法,該方法保證線程安全的獲取同步狀態,如果同步狀態獲取失敗,則構造同步節點(獨占式?Node.EXCLUSIVE,同一時刻只能有一個線程成功獲取同步狀態)并通過addWaiter(Node???????node)?方法將該節點加入到同步隊列的尾部,最后調用acquireQueued(Node?node,int?arg)方法,使得該節點以“死循環”的方式獲取同步狀態。如果獲取不到則阻塞節點中的線程,而被阻塞線程的喚醒主要依靠前驅節點的出隊或阻塞線程被中斷來實現。
下面分析一下相關工作。首先是節點的構造以及加入同步隊列,如下代碼所示。
上述代碼通過使用compareAndSetTail(Node?expect,Node?update)方法來確保節點能夠被線程安全添加。試想一下:如果使用一個普通的LinkedList來維護節點之間的關系,那么當一個線程獲取了同步狀態,而其他多個線程由于調用tryAcquire(int?arg)方法獲取同步狀態失敗而并發地被添加到LinkedList時,LinkedList將難以保證Node的正確添加,最終的結果可能是節點的數量有偏差,而且順序也是混亂的。
在enq(final?Node?node)方法中,同步器通過“死循環”來保證節點的正確添加,在“死循環”中只有通過CAS將節點設置成為尾節點之后,當前線程才能從該方法返回,否則,當前線程不斷地嘗試設置。可以看出,enq(final?Node?node)方法將并發添加節點的請求通過CAS變得“串行化”了。
節點進入同步隊列之后,就進入了一個自旋的過程,每個節點(或者說每個線程)都在自省地觀察,當條件滿足,獲取到了同步狀態,就可以從這個自旋過程中退出,否則依舊留在這個自旋過程中(并會阻塞節點的線程),如代碼所示。
在acquireQueued(final?Node?node,int?arg)方法中,當前線程在“死循環”中嘗試獲取同步狀態,而只有前驅節點是頭節點才能夠嘗試獲取同步狀態,這是為什么?原因有兩個,如下。
第一,頭節點是成功獲取到同步狀態的節點,而頭節點的線程釋放了同步狀態之后,將會
喚醒其后繼節點,后繼節點的線程被喚醒后需要檢查自己的前驅節點是否是頭節點。
第二,維護同步隊列的FIFO原則。該方法中,節點自旋獲取同步狀態的行為如圖所示。
在上圖中,由于非首節點線程前驅節點出隊或者被中斷而從等待狀態返回,隨后檢查自己的前驅是否是頭節點,如果是則嘗試獲取同步狀態。可以看到節點和節點之間在循環檢查的過程中基本不相互通信,而是簡單地判斷自己的前驅是否為頭節點,這樣就使得節點的釋放規則符合FIFO,并且也便于對過早通知的處理(過早通知是指前驅節點不是頭節點的線程由于中斷而被喚醒)。
獨占式同步狀態獲取流程,也就是acquire(int?arg)方法調用流程,如圖所示。
在上圖中,前驅節點為頭節點且能夠獲取同步狀態的判斷條件和線程進入等待狀態是獲取同步狀態的自旋過程。當同步狀態獲取成功之后,當前線程從acquire(int?arg)方法返回,如果對于鎖這種并發組件而言,代表著當前線程獲取了鎖。
當前線程獲取同步狀態并執行了相應邏輯之后,就需要釋放同步狀態,使得后續節點能夠繼續獲取同步狀態。通過調用同步器的release(int?arg)方法可以釋放同步狀態,該方法在釋放了同步狀態之后,會喚醒其后繼節點(進而使后繼節點重新嘗試獲取同步狀態)。該方法代碼如下所示。
該方法執行時,會喚醒頭節點的后繼節點線程,unparkSuccessor(Node?node)方法使用?LockSupport來喚醒處于等待狀態的線程。
分析了獨占式同步狀態獲取和釋放過程后,適當做個總結:在獲取同步狀態時,同步器維護一個同步隊列,獲取狀態失敗的線程都會被加入到隊列中并在隊列中進行自旋;移出隊列
(或停止自旋)的條件是前驅節點為頭節點且成功獲取了同步狀態。在釋放同步狀態時,同步器調用tryRelease(int?arg)方法釋放同步狀態,然后喚醒頭節點的后繼節點。
共享式同步狀態獲取與釋放
共享式獲取與獨占式獲取最主要的區別在于同一時刻能否有多個線程同時獲取到同步狀態。以文件的讀寫為例,如果一個程序在對文件進行讀操作,那么這一時刻對于該文件的寫操作均被阻塞,而讀操作能夠同時進行。寫操作要求對資源的獨占式訪問,而讀操作可以是共享式訪問,兩種不同的訪問模式在同一時刻對文件或資源的訪問情況,如圖所示。
在上圖中,左半部分,共享式訪問資源時,其他共享式的訪問均被允許,而獨占式訪問被阻塞,右半部分是獨占式訪問資源時,同一時刻其他訪問均被阻塞。
通過調用同步器的acquireShared(int?arg)方法可以共享式地獲取同步狀態,該方法代碼如下所示。
在acquireShared(int?arg)方法中,同步器調用tryAcquireShared(int?arg)方法嘗試獲取同步狀態,tryAcquireShared(int?arg)方法返回值為int類型,當返回值大于等于0時,表示能夠獲取到同步狀態。因此,在共享式獲取的自旋過程中,成功獲取到同步狀態并退出自旋的條件就是?tryAcquireShared(int?arg)方法返回值大于等于0。可以看到,在doAcquireShared(int?arg)方法的自旋過程中,如果當前節點的前驅為頭節點時,嘗試獲取同步狀態,如果返回值大于等于0,表示該次獲取同步狀態成功并從自旋過程中退出。
與獨占式一樣,共享式獲取也需要釋放同步狀態,通過調用releaseShared(int?arg)方法可以
釋放同步狀態,該方法代碼如下所示。
該方法在釋放同步狀態之后,將會喚醒后續處于等待狀態的節點。對于能夠支持多個線程同時訪問的并發組件(比如Semaphore),它和獨占式主要區別在于tryReleaseShared(int?arg)?方法必須確保同步狀態(或者資源數)線程安全釋放,一般是通過循環和CAS來保證的,因為釋放同步狀態的操作會同時來自多個線程。
獨占式超時獲取同步狀態
通過調用同步器的doAcquireNanos(int?arg,long?nanosTimeout)方法可以超時獲取同步狀態,即在指定的時間段內獲取同步狀態,如果獲取到同步狀態則返回true,否則,返回false。該方法提供了傳統Java同步操作(比如synchronized關鍵字)所不具備的特性。
在分析該方法的實現前,先介紹一下響應中斷的同步狀態獲取過程。在Java?5之前,當一個線程獲取不到鎖而被阻塞在synchronized之外時,對該線程進行中斷操作,此時該線程的中斷標志位會被修改,但線程依舊會阻塞在synchronized上,等待著獲取鎖。在Java?5中,同步器提供了acquireInterruptibly(int?arg)方法,這個方法在等待獲取同步狀態時,如果當前線程被中斷,會立刻返回,并拋出InterruptedException。
超時獲取同步狀態過程可以被視作響應中斷獲取同步狀態過程的“增強版”,?doAcquireNanos(int?arg,long?nanosTimeout)方法在支持響應中斷的基礎上,增加了超時獲取的特性。針對超時獲取,主要需要計算出需要睡眠的時間間隔nanosTimeout,為了防止過早通知,?nanosTimeout計算公式為:nanosTimeout-=now-lastTime,其中now為當前喚醒時間,lastTime為上次喚醒時間,如果nanosTimeout大于0則表示超時時間未到,需要繼續睡眠nanosTimeout納秒,反之,表示已經超時,該方法代碼如下所示。
該方法在自旋過程中,當節點的前驅節點為頭節點時嘗試獲取同步狀態,如果獲取成功則從該方法返回,這個過程和獨占式同步獲取的過程類似,但是在同步狀態獲取失敗的處理上有所不同。如果當前線程獲取同步狀態失敗,則判斷是否超時(nanosTimeout小于等于0表示已經超時),如果沒有超時,重新計算超時間隔nanosTimeout,然后使當前線程等待?nanosTimeout納秒(當已到設置的超時時間,該線程會從LockSupport.parkNanos(Object?blocker,long?nanos)方法返回)。
如果nanosTimeout小于等于spinForTimeoutThreshold(1000納秒)時,將不會使該線程進行超時等待,而是進入快速的自旋過程。原因在于,非常短的超時等待無法做到十分精確,如果這時再進行超時等待,相反會讓nanosTimeout的超時從整體上表現得反而不精確。因此,在超時非常短的場景下,同步器會進入無條件的快速自旋。
獨占式超時獲取同步態的流程如圖所示。
從圖中可以看出,獨占式超時獲取同步狀態doAcquireNanos(int?arg,long?nanosTimeout)?和獨占式獲取同步狀態acquire(int?args)在流程上非常相似,其主要區別在于未獲取到同步狀態時的處理邏輯。acquire(int?args)在未獲取到同步狀態時,將會使當前線程一直處于等待狀態,而doAcquireNanos(int?arg,long?nanosTimeout)會使當前線程等待nanosTimeout納秒,如果當?前線程在nanosTimeout納秒內沒有獲取到同步狀態,將會從等待邏輯中自動返回。