在開發(fā)Java多線程應(yīng)用程序中摸恍,各個線程之間由于要共享資源潮模,必須用到鎖機(jī)制存筏。Java提供了多種多線程鎖機(jī)制的實現(xiàn)方式,常見的有synchronized畏线、ReentrantLock静盅、Semaphore、AtomicInteger等寝殴。每種機(jī)制都有優(yōu)缺點與各自的適用場景蒿叠,必須熟練掌握他們的特點才能在Java多線程應(yīng)用開發(fā)時得心應(yīng)手。
更多Java鎖機(jī)制的詳細(xì)介紹參見文檔《Java鎖機(jī)制詳解》蚣常。
一栈虚、synchronized
幾乎每一個Java開發(fā)人員都認(rèn)識synchronized,使用它來實現(xiàn)多線程的同步操作是非常簡單的史隆,只要在需要同步的對方的方法魂务、類或代碼塊中加入該關(guān)鍵字,它能夠保證在同一個時刻最多只有一個線程執(zhí)行同一個對象的同步代碼泌射,可保證修飾的代碼在執(zhí)行過程中不會被其他線程干擾粘姜。使用synchronized修飾的代碼具有原子性和可見性,在需要進(jìn)程同步的程序中使用的頻率非常高熔酷,可以滿足一般的進(jìn)程同步要求(詳見《Java多線程基礎(chǔ)》)孤紧。
synchronized實現(xiàn)的機(jī)理依賴于軟件層面上的JVM,因此其性能會隨著Java版本的不斷升級而提高拒秘。事實上号显,在Java1.5中,synchronized是一個重量級操作躺酒,需要調(diào)用操作系統(tǒng)相關(guān)接口押蚤,性能是低效的,有可能給線程加鎖消耗的時間比有用操作消耗的時間更多羹应。到了Java1.6揽碘,synchronized進(jìn)行了很多的優(yōu)化,有適應(yīng)自旋园匹、鎖消除雳刺、鎖粗化、輕量級鎖及偏向鎖等裸违,效率有了本質(zhì)上的提高掖桦。在之后推出的Java1.7與1.8中,均對該關(guān)鍵字的實現(xiàn)機(jī)理做了優(yōu)化供汛。
需要說明的是枪汪,當(dāng)線程通過synchronized等待鎖時是不能被Thread.interrupt()中斷的涌穆,因此程序設(shè)計時必須檢查確保合理,否則可能會造成線程死鎖的尷尬境地料饥。
最后蒲犬,盡管Java實現(xiàn)的鎖機(jī)制有很多種,并且有些鎖機(jī)制性能也比synchronized高岸啡,但還是強(qiáng)烈推薦在多線程應(yīng)用程序中使用該關(guān)鍵字原叮,因為實現(xiàn)方便,后續(xù)工作由JVM來完成巡蘸,可靠性高奋隶。只有在確定鎖機(jī)制是當(dāng)前多線程程序的性能瓶頸時,才考慮使用其他機(jī)制悦荒,如ReentrantLock等唯欣。
二、ReentrantLock
可重入鎖搬味,顧名思義境氢,這個鎖可以被線程多次重復(fù)進(jìn)入進(jìn)行獲取操作。ReentantLock繼承接口Lock并實現(xiàn)了接口中定義的方法碰纬,除了能完成synchronized所能完成的所有工作外萍聊,還提供了諸如可響應(yīng)中斷鎖、可輪詢鎖請求悦析、定時鎖等避免多線程死鎖的方法寿桨。
Lock實現(xiàn)的機(jī)理依賴于特殊的CPU指定,可以認(rèn)為不受JVM的約束强戴,并可以通過其他語言平臺來完成底層的實現(xiàn)亭螟。在并發(fā)量較小的多線程應(yīng)用程序中,ReentrantLock與synchronized性能相差無幾骑歹,但在高并發(fā)量的條件下预烙,synchronized性能會迅速下降幾十倍,而ReentrantLock的性能卻能依然維持一個水準(zhǔn)陵刹,因此我們建議在高并發(fā)量情況下使用ReentrantLock默伍。
ReentrantLock引入兩個概念:公平鎖與非公平鎖。公平鎖指的是鎖的分配機(jī)制是公平的衰琐,通常先對鎖提出獲取請求的線程會先被分配到鎖。反之炼蹦,JVM按隨機(jī)羡宙、就近原則分配鎖的機(jī)制則稱為不公平鎖。ReentrantLock在構(gòu)造函數(shù)中提供了是否公平鎖的初始化方式掐隐,默認(rèn)為非公平鎖狗热。這是因為钞馁,非公平鎖實際執(zhí)行的效率要遠(yuǎn)遠(yuǎn)超出公平鎖,除非程序有特殊需要匿刮,否則最常用非公平鎖的分配機(jī)制僧凰。
ReentrantLock通過方法lock()與unlock()來進(jìn)行加鎖與解鎖操作,與synchronized會被JVM自動解鎖機(jī)制不同熟丸,ReentrantLock加鎖后需要手動進(jìn)行解鎖训措。為了避免程序出現(xiàn)異常而無法正常解鎖的情況,使用ReentrantLock必須在finally控制塊中進(jìn)行解鎖操作光羞。通常使用方式如下所示:
[](javascript:void(0); "復(fù)制代碼")
<pre>1 Lock lock = new ReentrantLock(); 2 try { 3 lock.lock(); 4 //...進(jìn)行任務(wù)操作
5 } finally { 6 lock.unlock(); 7 }</pre>
](javascript:void(0); "復(fù)制代碼")
下面我們詳細(xì)介紹有關(guān)ReentrantLock提供的可響應(yīng)中斷鎖、可輪詢鎖請求纱兑、定時鎖等機(jī)制與操作方式呀闻。
1捡多、線程在等待資源過程中需要中斷
ReentrantLock的在獲取鎖的過程中有2種鎖機(jī)制,忽略中斷鎖和響應(yīng)中斷鎖铐炫。當(dāng)?shù)却€程A或其他線程嘗試中斷線程A時,忽略中斷鎖機(jī)制則不會接收中斷驳遵,而是繼續(xù)處于等待狀態(tài);響應(yīng)中斷鎖則會處理這個中斷請求堤结,并將線程A由阻塞狀態(tài)喚醒為就緒狀態(tài),不再請求和等待資源竞穷。
lock.lock()可設(shè)置鎖機(jī)制為忽略中斷鎖,lock.lockInterruptibly()可設(shè)置鎖機(jī)制為響應(yīng)中斷鎖瘾带。下述例子描述了,一個寫線程和一個讀線程分別操作同一個同一個對象的寫方法和讀方法看政,寫方法需要執(zhí)行10秒時間朴恳,主線程中在啟動寫線程writer和讀線程reader后允蚣,啟動了第三個線程于颖,這個線程判斷當(dāng)程序執(zhí)行5秒后,如果讀線程依然處于等待狀態(tài)嚷兔,就將他中斷森渐,不再繼續(xù)等待資源做入。
View Code
由例子可知,ReentrantLock.lockInterruptibly()方法可設(shè)置線程在獲取鎖的時候響應(yīng)其他線程對當(dāng)前線程發(fā)出的中斷請求同衣。但必須注意竟块,此處響應(yīng)中斷鎖是指正在獲取鎖的過程中,如果線程此時并非處于獲取鎖的狀態(tài)耐齐,通過此方法設(shè)置是無法中斷線程的浪秘,非阻塞狀態(tài)可根據(jù)中斷標(biāo)記位Thread.currentThread().isInterrupted()在程序中手動設(shè)置中斷,阻塞狀態(tài)可通過拋出異常InterruptedException來中斷線程蚪缀,詳細(xì)可參考博文《Java多線程基礎(chǔ)》秫逝。
2、實現(xiàn)可輪詢的鎖請求
在synchronized中询枚,一旦發(fā)生死鎖违帆,唯一能夠恢復(fù)的辦法只能重新啟動程序,唯一的預(yù)防方法是在設(shè)計程序時考慮完善不要出錯金蜀。而有了Lock以后刷后,死鎖問題就有了新的預(yù)防辦法,它提供了tryLock()輪詢方法來獲得鎖渊抄,如果鎖可用則獲取鎖尝胆,如果鎖不可用,則此方法返回false护桦,并不會為了等待鎖而阻塞線程含衔,這極大地降低了死鎖情況的發(fā)生。典型使用語句如下:
View Code
3二庵、定時鎖請求
在synchronized中贪染,一旦發(fā)起鎖請求,該請求就不能停止了催享,如果不能獲得鎖杭隙,則當(dāng)前線程會阻塞并等待獲得鎖。在某些情況下因妙,你可能需要讓線程在一定時間內(nèi)去獲得鎖痰憎,如果在指定時間內(nèi)無法獲取鎖,則讓線程放棄鎖請求攀涵,轉(zhuǎn)而執(zhí)行其他的操作铣耘。Lock就提供了定時鎖的機(jī)制,使用Lock.tryLock(long timeout, TimeUnit unit)來指定讓線程在timeout單位時間內(nèi)去爭取鎖資源以故,如果超過這個時間仍然不能獲得鎖涡拘,則放棄鎖請求,定時鎖可以避免線程陷入死鎖的境地据德。
在上面的實例一中,其他線程在5秒后向正在等候鎖的讀線程發(fā)起中斷請求棘利,讀線程響應(yīng)請求并成功中斷善玫。也可以在讀線程中設(shè)置定時鎖,設(shè)定在5秒內(nèi)爭奪鎖蜗元,超時則放棄鎖系冗,并結(jié)束當(dāng)前的讀線程,使用定時鎖實現(xiàn)讀方法代碼如下:
View Code
三惯豆、Semaphore
上述兩種鎖機(jī)制類型都是“互斥鎖”楷兽,學(xué)過操作系統(tǒng)的都知道芯杀,互斥是進(jìn)程同步關(guān)系的一種特殊情況,相當(dāng)于只存在一個臨界資源柑蛇,因此同時最多只能給一個線程提供服務(wù)。但是诚欠,在實際復(fù)雜的多線程應(yīng)用程序中,可能存在多個臨界資源粉寞,這時候我們可以借助Semaphore信號量來完成多個臨界資源的訪問唧垦。
Semaphore基本能完成ReentrantLock的所有工作液样,使用方法也與之類似巧还,通過acquire()與release()方法來獲得和釋放臨界資源麸祷。經(jīng)實測褒搔,Semaphone.acquire()方法默認(rèn)為可響應(yīng)中斷鎖星瘾,與ReentrantLock.lockInterruptibly()作用效果一致,也就是說在等待臨界資源的過程中可以被Thread.interrupt()方法中斷磕瓷。
此外算撮,Semaphore也實現(xiàn)了可輪詢的鎖請求與定時鎖的功能肮柜,除了方法名tryAcquire與tryLock不同,其使用方法與ReentrantLock幾乎一致莱睁。Semaphore也提供了公平與非公平鎖的機(jī)制芒澜,也可在構(gòu)造函數(shù)中進(jìn)行設(shè)定。
Semaphore的鎖釋放操作也由手動進(jìn)行南吮,因此與ReentrantLock一樣部凑,為避免線程因拋出異常而無法正常釋放鎖的情況發(fā)生碧浊,釋放鎖的操作也必須在finally代碼塊中完成。
Semaphore支持多個臨界資源比勉,而ReentrantLock只支持一個臨界資源浩聋,筆者認(rèn)為ReentrantLock是Semaphore的一種特殊情況。Semaphore的使用方法與ReentrantLock實在太過相似嫂便,在此不再舉例說明闸与。
四践樱、AtomicInteger
首先說明拷邢,此處AtomicInteger是一系列相同類的代表之一屎慢,常見的還有AtomicLong、AtomicLong等环肘,他們的實現(xiàn)原理相同集灌,區(qū)別在與運算對象類型的不同欣喧。令人興奮地,還可以通過AtomicReference<V>將一個對象的所有操作轉(zhuǎn)化成原子操作益涧。
我們知道驯鳖,在多線程程序中,諸如++i 或 i++等運算不具有原子性嘹裂,是不安全的線程操作之一寄狼。通常我們會使用synchronized將該操作變成一個原子操作,但JVM為此類操作特意提供了一些同步類伊磺,使得使用更方便删咱,且使程序運行效率變得更高痰滋。通過相關(guān)資料顯示,通常AtomicInteger的性能是ReentantLock的好幾倍团搞。