The j.u.c Synchronizer Framework翻譯(一)背景與需求
摘要
在J2SE 1.5的java.util.concurrent包(下稱j.u.c包)中,大部分的同步器(例如鎖,屏障等等)都是基於AbstractQueuedSynchronizer(下稱AQS類)這個簡單的框架來構建的。這個框架為同步狀態的原子性管理、執行緒的阻塞和解除阻塞以及排隊提供了一種通用機制。這篇論文主要描述了這個框架基本原理、設計、實現、用法以及效能。
1. 背景介紹
通過JCP的JSR166規範,Java的1.5版本引入了j.u.c包,這個包提供了一系列支援中等程度併發的類。這些元件是一系列的同步器(抽象資料型別(ADT))。這些同步器主要維護著以下幾個功能:內部同步狀態的管理(例如:表示一個鎖的狀態是獲取還是釋放),同步狀態的更新和檢查操作,且至少有一個方法會導致呼叫執行緒在同步狀態被獲取時阻塞,以及在其他執行緒改變這個同步狀態時解除執行緒的阻塞。上述的這些的實際例子包括:互斥排它鎖的不同形式、讀寫鎖、訊號量、屏障、Future、事件指示器以及傳送佇列等。
幾乎任一同步器都可以用來實現其他形式的同步器。例如,可以用可重入鎖實現訊號量或者用訊號量實現可重入鎖。但是,這樣做帶來的複雜性,開銷,不靈活使其至多隻能是個二流工程。且缺乏吸引力。如果任何這樣的構造方式不能在本質上比其他形式更簡潔,那麼開發者就不應該隨意地選擇其中的某個來構建另一個同步器。取而代之,JSR166建立了一個小框架,AQS類。這個框架為構造同步器提供一種通用的機制,並且被j.u.c包中大部分類使用,同時很多使用者也用它來定義自己的同步器。
在這篇論文的下面部分會討論這個框架的需求、設計與實現背後的主要思路、示例用法,以及效能指標的一些測量。
2 需求
2.1 功能
同步器一般包含兩種方法,一種是acquire,另一種是release。acquire操作阻塞呼叫的執行緒,直到或除非同步狀態允許其繼續執行。而release操作則是通過某種方式改變同步狀態,使得一或多個被acquire阻塞的執行緒繼續執行。
j.u.c包中並沒有對同步器的API做一個統一的定義。因此,有一些類定義了通用的介面(如Lock),而另外一些則定義了其專有的版本。因此在不同的類中,acquire和release操作的名字和形式會各有不同。例如:Lock.lock,Semaphore.acquire,CountDownLatch.await和FutureTask.get,在這個框架裡,這些方法都是acquire操作。但是,J.U.C為支援一系列常見的使用選項,在類間都有個一致約定。在有意義的情況下,每一個同步器都支援下面的操作:
- 阻塞和非阻塞(例如tryLock)同步。
- 可選的超時設定,讓呼叫者可以放棄等待
- 通過中斷實現的任務取消,通常是分為兩個版本,一個acquire可取消,而另一個不可以。
同步器的實現根據其狀態是否獨佔而有所不同。獨佔狀態的同步器,在同一時間只有一個執行緒可以通過阻塞點,而共享狀態的同步器可以同時有多個執行緒在執行。一般鎖的實現類往往只維護獨佔狀態,但是,例如計數訊號量在數量許可的情況下,允許多個執行緒同時執行。為了使框架能得到廣泛應用,這兩種模式都要支援。
j.u.c包裡還定義了Condition介面,用於支援管程形式的await/signal操作,這些操作與獨佔模式的Lock類有關,且Condition的實現天生就和與其關聯的Lock類緊密相關。
2.2 效能目標
Java內建鎖(使用synchronized的方法或程式碼塊)的效能問題一直以來都在被人們關注,並且已經有一系列的文章描述其構造(例如引文[1],[3])。然而,大部分的研究主要關注的是在單核處理器上大部分時候使用於單執行緒上下文環境中時,如何儘量降低其空間(因為任何的Java物件都可以當成是鎖)和時間的開銷。對於同步器來說這些都不是特別重要:程式設計師僅在需要的時候才會使用同步器,因此並不需要壓縮空間來避免浪費,並且同步器幾乎是專門用在多執行緒設計中(特別是在多核處理器上),在這種環境下,偶爾的競爭是在意料之中的。因此,常規的JVM鎖優化策略主要是針對零競爭的場景,而其它場景則使用缺乏可預見性的“慢速路徑(slow paths)” ,所以常規的JVM鎖優化策略並不適用於嚴重依賴於J.U.C包的典型多執行緒服務端應用。
這裡主要的效能目標是可伸縮性,即在大部分情況下,即使,或特別在同步器有競爭的情況下,穩定地保證其效率。在理想的情況下,不管有多少執行緒正試圖通過同步點,通過同步點的開銷都應該是個常量。在某一執行緒被允許通過同步點但還沒有通過的情況下,使其耗費的總時間最少,這是主要目標之一。然而,這也必須考慮平衡各種資源,包括總CPU時間的需求,記憶體負載以及執行緒排程的開銷。例如:獲取自旋鎖通常比阻塞鎖所需的時間更短,但是通常也會浪費CPU時鐘週期,並且造成記憶體競爭,所以使用的並不頻繁。
實現同步器的這些目標包含了兩種不同的使用型別。大部分應用程式是最大化其總的吞吐量,容錯性,並且最好保證儘量減少飢餓的情況。然而,對於那些控制資源分配的程式來說,更重要是去維持多執行緒讀取的公平性,可以接受較差的總吞吐量。沒有任何框架可以代表使用者去決定應該選擇哪一個方式,因此,應該提供不同的公平策略。
無論同步器的內部實現是多麼的精雕細琢,它還是會在某些應用中產生效能瓶頸。因此,框架必須提供相應的監視工具讓使用者發現和緩和這些瓶頸。至少需要提供一種方式來確定有多少執行緒被阻塞了。