在j2se 1.5的java.util.concurrent包(下稱j.u.c包)中,大部分的同步器(例如鎖,屏障等等)都是基于abstractqueuedsynchronizer(下稱aqs類)這個簡單的架構來建構的。這個架構為同步狀态的原子性管理、線程的阻塞和解除阻塞以及排隊提供了一種通用機制。這篇論文主要描述了這個架構基本原理、設計、實作、用法以及性能。
通過jcp的jsr166規範,java的1.5版本引入了j.u.c包,這個包提供了一系列支援中等程度并發的類。這些元件是一系列的同步器(抽象資料類型(adt))。這些同步器主要維護着以下幾個功能:内部同步狀态的管理(例如:表示一個鎖的狀态是擷取還是釋放),同步狀态的更新和檢查操作,且至少有一個方法會導緻調用線程在同步狀态被擷取時阻塞,以及在其他線程改變這個同步狀态時解除線程的阻塞。上述的這些的實際例子包括:互斥排它鎖的不同形式、讀寫鎖、信号量、屏障、future、事件訓示器以及傳送隊列等。
幾乎任一同步器都可以用來實作其他形式的同步器。例如,可以用可重入鎖實作信号量或者用信号量實作可重入鎖。但是,這樣做帶來的複雜性,開銷,不靈活使其至多隻能是個二流工程。且缺乏吸引力。如果任何這樣的構造方式不能在本質上比其他形式更簡潔,那麼開發者就不應該随意地選擇其中的某個來建構另一個同步器。取而代之,jsr166建立了一個小架構,aqs類。這個架構為構造同步器提供一種通用的機制,并且被j.u.c包中大部分類使用,同時很多使用者也用它來定義自己的同步器。
在這篇論文的下面部分會讨論這個架構的需求、設計與實作背後的主要思路、示例用法,以及性能名額的一些測量。
同步器一般包含兩種方法,一種是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類緊密相關。
java内置鎖(使用synchronized的方法或代碼塊)的性能問題一直以來都在被人們關注,并且已經有一系列的文章描述其構造(例如引文[1],[3])。然而,大部分的研究主要關注的是在單核處理器上大部分時候使用于單線程上下文環境中時,如何盡量降低其空間(因為任何的java對象都可以當成是鎖)和時間的開銷。對于同步器來說這些都不是特别重要:程式員僅在需要的時候才會使用同步器,是以并不需要壓縮空間來避免浪費,并且同步器幾乎是專門用在多線程設計中(特别是在多核處理器上),在這種環境下,偶爾的競争是在意料之中的。是以,正常的jvm鎖優化政策主要是針對零競争的場景,而其它場景則使用缺乏可預見性的“慢速路徑(slow paths)” ,是以正常的jvm鎖優化政策并不适用于嚴重依賴于j.u.c包的典型多線程服務端應用。
這裡主要的性能目标是可伸縮性,即在大部分情況下,即使,或特别在同步器有競争的情況下,穩定地保證其效率。在理想的情況下,不管有多少線程正試圖通過同步點,通過同步點的開銷都應該是個常量。在某一線程被允許通過同步點但還沒有通過的情況下,使其耗費的總時間最少,這是主要目标之一。然而,這也必須考慮平衡各種資源,包括總cpu時間的需求,記憶體負載以及線程排程的開銷。例如:擷取自旋鎖通常比阻塞鎖所需的時間更短,但是通常也會浪費cpu時鐘周期,并且造成記憶體競争,是以使用的并不頻繁。
實作同步器的這些目标包含了兩種不同的使用類型。大部分應用程式是最大化其總的吞吐量,容錯性,并且最好保證盡量減少饑餓的情況。然而,對于那些控制資源配置設定的程式來說,更重要是去維持多線程讀取的公平性,可以接受較差的總吞吐量。沒有任何架構可以代表使用者去決定應該選擇哪一個方式,是以,應該提供不同的公平政策。
無論同步器的内部實作是多麼的精雕細琢,它還是會在某些應用中産生性能瓶頸。是以,架構必須提供相應的監視工具讓使用者發現和緩和這些瓶頸。至少需要提供一種方式來确定有多少線程被阻塞了。