在開發(fā)Java多線程應(yīng)用程序中,各個(gè)線程之間由于要共享資源,必須用到鎖機(jī)制。Java提供了多種多線程鎖機(jī)制的實(shí)現(xiàn)方式,常見的有synchronized、ReentrantLock、Semaphore、AtomicInteger等。每種機(jī)制都有優(yōu)缺點(diǎn)與各自的適用場(chǎng)景,必須熟練掌握他們的特點(diǎn)才能在Java多線程應(yīng)用開發(fā)時(shí)得心應(yīng)手。
更多Java鎖機(jī)制的詳細(xì)介紹參見文檔《Java鎖機(jī)制詳解》。
一、synchronized
幾乎每一個(gè)Java開發(fā)人員都認(rèn)識(shí)synchronized,使用它來實(shí)現(xiàn)多線程的同步操作是非常簡(jiǎn)單的,只要在需要同步的對(duì)方的方法、類或代碼塊中加入該關(guān)鍵字,它能夠保證在同一個(gè)時(shí)刻最多只有一個(gè)線程執(zhí)行同一個(gè)對(duì)象的同步代碼,可保證修飾的代碼在執(zhí)行過程中不會(huì)被其他線程干擾。使用synchronized修飾的代碼具有原子性和可見性,在需要進(jìn)程同步的程序中使用的頻率非常高,可以滿足一般的進(jìn)程同步要求(詳見《Java多線程基礎(chǔ)》)。
synchronized實(shí)現(xiàn)的機(jī)理依賴于軟件層面上的JVM,因此其性能會(huì)隨著Java版本的不斷升級(jí)而提高。事實(shí)上,在Java1.5中,synchronized是一個(gè)重量級(jí)操作,需要調(diào)用操作系統(tǒng)相關(guān)接口,性能是低效的,有可能給線程加鎖消耗的時(shí)間比有用操作消耗的時(shí)間更多。到了Java1.6,synchronized進(jìn)行了很多的優(yōu)化,有適應(yīng)自旋、鎖消除、鎖粗化、輕量級(jí)鎖及偏向鎖等,效率有了本質(zhì)上的提高。在之后推出的Java1.7與1.8中,均對(duì)該關(guān)鍵字的實(shí)現(xiàn)機(jī)理做了優(yōu)化。
需要說明的是,當(dāng)線程通過synchronized等待鎖時(shí)是不能被Thread.interrupt()中斷的,因此程序設(shè)計(jì)時(shí)必須檢查確保合理,否則可能會(huì)造成線程死鎖的尷尬境地。
最后,盡管Java實(shí)現(xiàn)的鎖機(jī)制有很多種,并且有些鎖機(jī)制性能也比synchronized高,但還是強(qiáng)烈推薦在多線程應(yīng)用程序中使用該關(guān)鍵字,因?yàn)閷?shí)現(xiàn)方便,后續(xù)工作由JVM來完成,可靠性高。只有在確定鎖機(jī)制是當(dāng)前多線程程序的性能瓶頸時(shí),才考慮使用其他機(jī)制,如ReentrantLock等。
二、ReentrantLock
可重入鎖,顧名思義,這個(gè)鎖可以被線程多次重復(fù)進(jìn)入進(jìn)行獲取操作。ReentantLock繼承接口Lock并實(shí)現(xiàn)了接口中定義的方法,除了能完成synchronized所能完成的所有工作外,還提供了諸如可響應(yīng)中斷鎖、可輪詢鎖請(qǐng)求、定時(shí)鎖等避免多線程死鎖的方法。
Lock實(shí)現(xiàn)的機(jī)理依賴于特殊的CPU指定,可以認(rèn)為不受JVM的約束,并可以通過其他語(yǔ)言平臺(tái)來完成底層的實(shí)現(xiàn)。在并發(fā)量較小的多線程應(yīng)用程序中,ReentrantLock與synchronized性能相差無幾,但在高并發(fā)量的條件下,synchronized性能會(huì)迅速下降幾十倍,而ReentrantLock的性能卻能依然維持一個(gè)水準(zhǔn),因此我們建議在高并發(fā)量情況下使用ReentrantLock。
ReentrantLock引入兩個(gè)概念:公平鎖與非公平鎖。公平鎖指的是鎖的分配機(jī)制是公平的,通常先對(duì)鎖提出獲取請(qǐng)求的線程會(huì)先被分配到鎖。反之,JVM按隨機(jī)、就近原則分配鎖的機(jī)制則稱為不公平鎖。ReentrantLock在構(gòu)造函數(shù)中提供了是否公平鎖的初始化方式,默認(rèn)為非公平鎖。這是因?yàn)?,非公平鎖實(shí)際執(zhí)行的效率要遠(yuǎn)遠(yuǎn)超出公平鎖,除非程序有特殊需要,否則最常用非公平鎖的分配機(jī)制。
ReentrantLock通過方法lock()與unlock()來進(jìn)行加鎖與解鎖操作,與synchronized會(huì)被JVM自動(dòng)解鎖機(jī)制不同,ReentrantLock加鎖后需要手動(dòng)進(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ù)制代碼")
下面我們?cè)敿?xì)介紹有關(guān)ReentrantLock提供的可響應(yīng)中斷鎖、可輪詢鎖請(qǐng)求、定時(shí)鎖等機(jī)制與操作方式。
1、線程在等待資源過程中需要中斷
ReentrantLock的在獲取鎖的過程中有2種鎖機(jī)制,忽略中斷鎖和響應(yīng)中斷鎖。當(dāng)?shù)却€程A或其他線程嘗試中斷線程A時(shí),忽略中斷鎖機(jī)制則不會(huì)接收中斷,而是繼續(xù)處于等待狀態(tài);響應(yīng)中斷鎖則會(huì)處理這個(gè)中斷請(qǐng)求,并將線程A由阻塞狀態(tài)喚醒為就緒狀態(tài),不再請(qǐng)求和等待資源。
lock.lock()可設(shè)置鎖機(jī)制為忽略中斷鎖,lock.lockInterruptibly()可設(shè)置鎖機(jī)制為響應(yīng)中斷鎖。下述例子描述了,一個(gè)寫線程和一個(gè)讀線程分別操作同一個(gè)同一個(gè)對(duì)象的寫方法和讀方法,寫方法需要執(zhí)行10秒時(shí)間,主線程中在啟動(dòng)寫線程writer和讀線程reader后,啟動(dòng)了第三個(gè)線程,這個(gè)線程判斷當(dāng)程序執(zhí)行5秒后,如果讀線程依然處于等待狀態(tài),就將他中斷,不再繼續(xù)等待資源。

View Code
由例子可知,ReentrantLock.lockInterruptibly()方法可設(shè)置線程在獲取鎖的時(shí)候響應(yīng)其他線程對(duì)當(dāng)前線程發(fā)出的中斷請(qǐng)求。但必須注意,此處響應(yīng)中斷鎖是指正在獲取鎖的過程中,如果線程此時(shí)并非處于獲取鎖的狀態(tài),通過此方法設(shè)置是無法中斷線程的,非阻塞狀態(tài)可根據(jù)中斷標(biāo)記位Thread.currentThread().isInterrupted()在程序中手動(dòng)設(shè)置中斷,阻塞狀態(tài)可通過拋出異常InterruptedException來中斷線程,詳細(xì)可參考博文《Java多線程基礎(chǔ)》。
2、實(shí)現(xiàn)可輪詢的鎖請(qǐng)求
在synchronized中,一旦發(fā)生死鎖,唯一能夠恢復(fù)的辦法只能重新啟動(dòng)程序,唯一的預(yù)防方法是在設(shè)計(jì)程序時(shí)考慮完善不要出錯(cuò)。而有了Lock以后,死鎖問題就有了新的預(yù)防辦法,它提供了tryLock()輪詢方法來獲得鎖,如果鎖可用則獲取鎖,如果鎖不可用,則此方法返回false,并不會(huì)為了等待鎖而阻塞線程,這極大地降低了死鎖情況的發(fā)生。典型使用語(yǔ)句如下:

View Code
3、定時(shí)鎖請(qǐng)求
在synchronized中,一旦發(fā)起鎖請(qǐng)求,該請(qǐng)求就不能停止了,如果不能獲得鎖,則當(dāng)前線程會(huì)阻塞并等待獲得鎖。在某些情況下,你可能需要讓線程在一定時(shí)間內(nèi)去獲得鎖,如果在指定時(shí)間內(nèi)無法獲取鎖,則讓線程放棄鎖請(qǐng)求,轉(zhuǎn)而執(zhí)行其他的操作。Lock就提供了定時(shí)鎖的機(jī)制,使用Lock.tryLock(long timeout, TimeUnit unit)來指定讓線程在timeout單位時(shí)間內(nèi)去爭(zhēng)取鎖資源,如果超過這個(gè)時(shí)間仍然不能獲得鎖,則放棄鎖請(qǐng)求,定時(shí)鎖可以避免線程陷入死鎖的境地。
在上面的實(shí)例一中,其他線程在5秒后向正在等候鎖的讀線程發(fā)起中斷請(qǐng)求,讀線程響應(yīng)請(qǐng)求并成功中斷。也可以在讀線程中設(shè)置定時(shí)鎖,設(shè)定在5秒內(nèi)爭(zhēng)奪鎖,超時(shí)則放棄鎖,并結(jié)束當(dāng)前的讀線程,使用定時(shí)鎖實(shí)現(xiàn)讀方法代碼如下:

View Code
三、Semaphore
上述兩種鎖機(jī)制類型都是“互斥鎖”,學(xué)過操作系統(tǒng)的都知道,互斥是進(jìn)程同步關(guān)系的一種特殊情況,相當(dāng)于只存在一個(gè)臨界資源,因此同時(shí)最多只能給一個(gè)線程提供服務(wù)。但是,在實(shí)際復(fù)雜的多線程應(yīng)用程序中,可能存在多個(gè)臨界資源,這時(shí)候我們可以借助Semaphore信號(hào)量來完成多個(gè)臨界資源的訪問。
Semaphore基本能完成ReentrantLock的所有工作,使用方法也與之類似,通過acquire()與release()方法來獲得和釋放臨界資源。經(jīng)實(shí)測(cè),Semaphone.acquire()方法默認(rèn)為可響應(yīng)中斷鎖,與ReentrantLock.lockInterruptibly()作用效果一致,也就是說在等待臨界資源的過程中可以被Thread.interrupt()方法中斷。
此外,Semaphore也實(shí)現(xiàn)了可輪詢的鎖請(qǐng)求與定時(shí)鎖的功能,除了方法名tryAcquire與tryLock不同,其使用方法與ReentrantLock幾乎一致。Semaphore也提供了公平與非公平鎖的機(jī)制,也可在構(gòu)造函數(shù)中進(jìn)行設(shè)定。
Semaphore的鎖釋放操作也由手動(dòng)進(jìn)行,因此與ReentrantLock一樣,為避免線程因拋出異常而無法正常釋放鎖的情況發(fā)生,釋放鎖的操作也必須在finally代碼塊中完成。
Semaphore支持多個(gè)臨界資源,而ReentrantLock只支持一個(gè)臨界資源,筆者認(rèn)為ReentrantLock是Semaphore的一種特殊情況。Semaphore的使用方法與ReentrantLock實(shí)在太過相似,在此不再舉例說明。
四、AtomicInteger
首先說明,此處AtomicInteger是一系列相同類的代表之一,常見的還有AtomicLong、AtomicLong等,他們的實(shí)現(xiàn)原理相同,區(qū)別在與運(yùn)算對(duì)象類型的不同。令人興奮地,還可以通過AtomicReference<V>將一個(gè)對(duì)象的所有操作轉(zhuǎn)化成原子操作。
我們知道,在多線程程序中,諸如++i 或 i++等運(yùn)算不具有原子性,是不安全的線程操作之一。通常我們會(huì)使用synchronized將該操作變成一個(gè)原子操作,但JVM為此類操作特意提供了一些同步類,使得使用更方便,且使程序運(yùn)行效率變得更高。通過相關(guān)資料顯示,通常AtomicInteger的性能是ReentantLock的好幾倍。