最近在使用memcache客戶端的時候,發(fā)現(xiàn)一個可能是多線程的問題,客戶端的實現(xiàn)是NIO+JUC,由于出現(xiàn)頻率很低,場景沒有辦法復原,一直沒有找到問題的真正原因,通過代碼走查也沒有發(fā)現(xiàn)任何問題,于是決定回顧一下JUC的東西,看看是不是可以受到啟發(fā),于是決定先看一下大牛Doug Lea的論文,順便翻譯一下。由于英文水平很挫,又是第一次,希望不要誤導了大家。廢話不表。
?
?JAVA.util.concurrent 同步框架
?
Doug Lea SUNY Oswego ?Oswego NY 13126 ?dl@cs.oswego.edu
?
摘要
? ? ?J2SE1.5的java.util.concurrent包中大多數(shù)同步器(鎖,壁壘(barrier)等)都是基于一個輕量級框架建立起來的,這個框架的基礎(chǔ)類就是AbstractQueuedSynchronizer類。
這個框架為同步狀態(tài)的原子性操作、阻塞和喚醒線程和排隊提供了通用的機制。本文將介紹這個框架的基本原理、設(shè)計、實現(xiàn)、使用和性能。
?
分類和主題
?
D.1.3 [編程技術(shù)]:并發(fā)編程 – 并行編程
?
通用術(shù)語
算法、測量(measurement)、性能、設(shè)計。
?
keywords
? 同步、Java
?
1、引言
? ? 通過Java社區(qū)進程(JCP)的Java規(guī)范請求(JSR)166,java 1.5引入了java.util.concurrent包,它提供了一系列中等水平的并發(fā)支持類,這些組件是一系列的同步器,即一個維護了內(nèi)部同步狀態(tài)的抽象數(shù)據(jù)類型(ADT)(例如:表示一個鎖是鎖定還是解鎖)。可以對這個狀態(tài)的更新和檢查,并且必須至少提供一個方法,調(diào)用它可以阻塞線程。并且當其它線程改變這個狀態(tài)的時候,允許該線程喚醒。例如:各種形式的互斥鎖、讀寫鎖、信號量(semaphores)、 壁壘(barrier)、futures、 事件信號(event ?indicators)、?和交替隊列(handoff queues)。
? ??眾所周知(見[2])幾乎任何同步器都可用于實現(xiàn)其他的同步器,例如:可以通過可重入鎖(reentrant locks)構(gòu)建信號量(semaphores ),反之亦然;然而,這樣的實現(xiàn)往往都非常復雜,過度的設(shè)計和僵化的實現(xiàn),充其量只能是備選方案;此外在概念上也不具有吸引力,如果這些結(jié)構(gòu)與其他沒有本質(zhì)上的區(qū)別,開發(fā)人員不應該被強迫選擇任意其中之一作為建設(shè)其他的基礎(chǔ);相反,JSR166建立一個以類AbstractQueuedSynchronizer為中心的輕量級框架,它提供了一個公共的機制,包里面提供的同步器都是基于它,當然用戶也可以定義自己的類;
? ?本文的其余部分討論了這個框架的需求,設(shè)計、實現(xiàn)和用法示例,以及一些其性能特點;
?
2、需求
2.1、功能需求
?
? ? ?同步器具有兩種類型的方法[7]:至少有一個獲取鎖操作(acquire ),它阻塞線程直到同步狀態(tài)允許繼續(xù)執(zhí)行,另外需要一個釋放操作(release),它改變同步的狀態(tài),以允許一個或多個阻塞的線程解鎖。java.util.concurrent包中沒有為同步器定義一個統(tǒng)一的API。一些定義是通過通用的接口(如鎖),另外一些只是包含在特定的版本中;因此,獲取和釋放操作,在不同的類中的名稱和形式不一樣。例如,Lock.lock Semaphore.acquire,CountDownLatch.await和FutureTask.get方法都是獲取操作。然而,不同的類也保持一致的約定,以支持一系列常見的用法。在有意義的時候,每個同步支持:
? 1、非阻塞同步(例如的tryLock)以及阻斷版本;
? 2、可選的超時,這樣應用程序可以放棄等待;
? 3、通過中斷(interruption)實現(xiàn)可取消,通常提供一個可中斷的和不可中斷的獲取操作(acquire )
? ?根據(jù)他們是否只維護一個互斥量(exclusive),同步器可能會有所不同;互斥量(exclusive)表示在這個可能的阻塞點一次只有一個線程可以執(zhí)行;對應還有的阻塞點可以允許一次至少一個線程允許。他們叫共享量(shared);通常的鎖都是獨占的(擁有一個互斥量),但是像計數(shù)器。可以允許多個允許計數(shù)的線程同時獲取;要想廣泛使用,該框架必須支持兩種操作模式。
? ? java.util.concurrent包中還定義了Condition接口,提供監(jiān)視器風格的阻塞(await)和喚醒(signal)操作,可以在獨占類型的鎖里使用,它的實現(xiàn)本質(zhì)上是依賴于他關(guān)聯(lián)的鎖;
2.2 性能需求
? ? ?Java內(nèi)置鎖(使用synchronized方法或者synchronized塊)開發(fā)者長期以來一直擔心它的性能,關(guān)于他的研究文獻也相當可觀([1], [3]),然而,這些工作的主要重點是在單處理器單線程的上下文中使用時最大限度地減少空間上的開銷(因為任何Java對象可以作為一個鎖)和最大限度地減少時間開銷;但是這些都不是同步器應該關(guān)心的重點:1、程序員只在需要時構(gòu)建同步器,所以沒有必要壓縮空間;2、 可以預料同步器幾乎全部用在多線程設(shè)計(多處理器的場景也越來越多)。通常JVM的優(yōu)化,也只是針對零競爭的場景,其他的場景是很難預見和處理的; "slow paths" [12] is not the right tactic for typical multithreaded server applications that rely heavily on java.util.concurrent.(暫時沒想到好的翻譯)
? ? ?相反,這里的主要目標是可擴展性:特別是當使用同步器是有爭議的時候可以預測維護效率。理想的情況下,不管多少線程同步,在一個同步點上的開銷應該是恒定的。其中的主要目標是,以盡量減少總時間,在此期間,一個線程允許通過一個同步點,但其他將會阻塞。當然,這必須兼顧對其他資源的考慮,比如:總CPU時間,內(nèi)存開銷,和線程調(diào)度的開銷。舉例來說,自旋鎖(spinlocks)通常比阻塞鎖提供更短的獲取時間,但是通常因為空循環(huán)和內(nèi)存爭用而不經(jīng)常使用。
? ? ?這些目標覆蓋了兩種使用風格,大多數(shù)應用程序追求最大化的總吞吐量,容忍饑餓的出現(xiàn),然而另外一些應用,如資源控制,對他們來說更為重要的是保持線程的公平性,允許低的總吞吐量。 框架不可以代替用戶決定這些相互沖突的目標,而是必須實現(xiàn)不同的公平策略。
? ? ?不管如何精心設(shè)計的,對于某些應用,同步器將出現(xiàn)性能瓶頸;因此,框架必須提供可監(jiān)測和檢查的基本操作,讓用戶及時發(fā)現(xiàn)和緩解瓶頸;最基本的功能(也是最有用)需要提供一種方法來確定有多少線程被阻塞;
3、設(shè)計和實現(xiàn)
? ? 一個同步器背后的基本理念是非常簡單。一個獲取操作(acquire)的處理如下:
?
while (同步狀態(tài)不能獲取(acquire)) { 當前線程如沒有排隊等待,則排隊 可能阻塞當前線程; } 如果當前線程排隊,則出隊列;
? ? ?一個釋放(release)操作的處理流程如下:
?
更新同步器狀態(tài) if(同步狀態(tài)允許一個阻塞的線程獲取(acquire)) 喚醒一個或者多個線程?
? 支持這些操作需要下面三個基本組成部分的協(xié)調(diào):
? 同步狀態(tài)的原子管理
? 線程阻塞和解除阻塞
? 維護一個隊列
? ? ?雖然可以建立一個框架,使這三件獨立變化;然而,這既不是非常有效的,也是不可用的。例如,存放在隊列節(jié)點的信息必須與需要釋放的線程關(guān)聯(lián),并且暴露的方法簽名必須依賴于同步狀態(tài)的特性。同步框架的核心設(shè)計決策是為這三個組成部分選擇一個具體實現(xiàn),同時仍保持一個靈活的擴展;雖然限制了適用范圍,但提供了高效率的支持,幾乎沒有任何理由不使用它,而去從頭開始構(gòu)建同步器。
?
3.1 同步狀態(tài)
? ? ?類AbstractQueuedSynchronizer使用一個(32位)的整數(shù)維護同步狀態(tài),并且提供getState,setState和compareAndSetState方法訪問和更新狀態(tài);反過來,
java.util.concurrent.atomic的這些方法支持JSR133(Java內(nèi)存模型)定義的讀取和寫入操作的可見性(volatile)語義;并且通過一個native的?
compare-and-swap 和 loadlinked/ store-conditional 來實現(xiàn) compare- AndSetState方法;只有當它擁有的值和給定的預期相同,才會更新為一個新的值,整個操作保持原子性。
? ? ?以一個32位的int維護同步狀態(tài),是一個務(wù)實的決定。雖然JSR166還提供了64位長的原子操作,但是這些仍然必須在適當?shù)钠脚_上使用,用來模擬內(nèi)部鎖。否則同步器可能不會工作的很好,在將來,很可能添加第二個基類提供一個專門的64位狀態(tài)(即 long 型);但是,現(xiàn)在沒有一個令人信服的理由,將它包含在包里;目前,32位滿足大多數(shù)應用,
java.util.concurrent中只有一個CyclicBarrier同步類,維護更多的位來保持狀態(tài),代替使用鎖(它像大多數(shù)更高級別的工具包)。
? ? ?基于類AbstractQueuedSynchronizer具體實現(xiàn)必須定義tryAcquire和tryRelease方法,用這些對外暴露的方法控制獲取和釋放操作。如果tryAcquire方法獲取(acquire)成功必須返回true,如果新的同步狀態(tài)允許新的線程獲取,tryRelease方法必須返回true,這些方法只接受一個int參數(shù),用于各自狀態(tài)的溝通,例如, 可重入鎖( reentrant lock),當?shù)却龡l件返回后重新獲得鎖需要重新建立遞歸計數(shù)。許多同步器并不需要這個參數(shù),可以直接忽略它。
3.2 阻塞:
? ? ?JSR166之前,沒有Java API可以阻塞和喚醒線程,用于構(gòu)建一個不基于內(nèi)在的監(jiān)視器(monitors.)的同步機制。唯一可以使用的是Thread.suspend 和Thread.resume,
但是由于他們有一個無法解決的競爭問題,也難以使用,即:如果一個解除阻塞的線程在阻塞線程被暫停(suspend)之前執(zhí)行恢復操作(resume),恢復操作(resume)不會有任何效果。java.util.concurrent.locks包里的LockSupport類提供了解決這個問題的方案。方法LockSupport.park阻塞當前線程,直到LockSupport.unpark被調(diào)用,(假喚醒也是允許的)unpark方法的調(diào)用不會計數(shù),所以在一個park方法前多個unpark,也只喚醒一個park方法阻塞的線程。此外,這適用于每個線程,而不是每同步。這意味著一個線程在一個新的同步器上調(diào)用了park方法,可能會立即返回,因為有之前剩余的unpark,然而,在一個unpark的情況下,它的下一次調(diào)用將被阻塞。雖然有可能顯式地清除這種狀態(tài),這是不值得這樣做。這使得當需要多次調(diào)用park的時候更有效。
? ? ?同樣的機制也一定程度上被Solaris9線程庫[11]、 win32的“事件消費機制”、和Linux的NPTL線程庫等使用。每一個對應到在最常見的Java平臺上運行也是有效的。
(目前Sun HotSpot JVM的實現(xiàn)參考了Solaris和Linux上實際使用的一個pthread condvar機制來兼容現(xiàn)有的設(shè)計。)park方法同樣支持一個可選項,比如一個相對或則絕對的超時時間(timeout),并且集成了JVM 中Thread.interrupt支持 - 線程unparks的時候可以中斷它。
?
原文見附件
?
下一篇:http://caoyaojun1988-163-com.iteye.com/admin/blogs/1290759
-
本文附件下載:
- aqs.pdf (289.9 KB)