0x01:synchronized
在Java中synchronized關鍵字被常用于維護數據一致性。
synchronized機制是給共享資源上鎖,只有拿到鎖的線程才可以訪問共享資源,這樣就可以強制使得對共享資源的訪問都是順序的。
Java開發人員都認識synchronized,使用它來實現多線程的同步操作是非常簡單的,只要在需要同步的對方的方法、類或代碼塊中加入該關鍵字,它能夠保證在同一個時刻最多只有一個線程執行同一個對象的同步代碼,可保證修飾的代碼在執行過程中不會被其他線程幹擾。使用synchronized修飾的代碼具有原子性和可見性,在需要進程同步的程序中使用的頻率非常高,可以滿足一般的進程同步要求。
synchronized (obj) {
//方法
…….
}
synchronized實現的機理依賴于軟件層面上的JVM,因此其性能會隨著Java版本的不斷升級而提高。
到了Java1.6,synchronized進行了很多的優化,有適應自旋、鎖消除、鎖粗化、輕量級鎖及偏向鎖等,效率有了本質上的提高。在之後推出的Java1.7與1.8中,均對該關鍵字的實現機理做了優化。
需要說明的是,當線程通過synchronized等待鎖時是不能被Thread.interrupt()中斷的,因此程序設計時必須檢查確保合理,否則可能會造成線程死鎖的尴尬境地。
最後,盡管Java實現的鎖機制有很多種,並且有些鎖機制性能也比synchronized高,但還是強烈推薦在多線程應用程序中使用該關鍵字,因爲實現方便,後續工作由JVM來完成,可靠性高。只有在確定鎖機制是當前多線程程序的性能瓶頸時,才考慮使用其他機制,如ReentrantLock等。
0x02:ReentrantLock
可重入鎖,顧名思義,這個鎖可以被線程多次重複進入進行獲取操作。
ReentantLock繼承接口Lock並實現了接口中定義的方法,除了能完成synchronized所能完成的所有工作外,還提供了諸如可響應中斷鎖、可輪詢鎖請求、定時鎖等避免多線程死鎖的方法。
Lock實現的機理依賴于特殊的CPU指定,可以認爲不受JVM的約束,並可以通過其他語言平台來完成底層的實現。在並發量較小的多線程應用程序中,ReentrantLock與synchronized性能相差無幾,但在高並發量的條件下,synchronized性能會迅速下降幾十倍,而ReentrantLock的性能卻能依然維持一個水准。
因此我們建議在高並發量情況下使用ReentrantLock。
ReentrantLock引入兩個概念:公平鎖與非公平鎖。
公平鎖指的是鎖的分配機制是公平的,通常先對鎖提出獲取請求的線程會先被分配到鎖。反之,JVM按隨機、就近原則分配鎖的機制則稱爲不公平鎖。
ReentrantLock在構造函數中提供了是否公平鎖的初始化方式,默認爲非公平鎖。這是因爲,非公平鎖實際執行的效率要遠遠超出公平鎖,除非程序有特殊需要,否則最常用非公平鎖的分配機制。
ReentrantLock通過方法lock()與unlock()來進行加鎖與解鎖操作,與synchronized會被JVM自動解鎖機制不同,ReentrantLock加鎖後需要手動進行解鎖。爲了避免程序出現異常而無法正常解鎖的情況,使用ReentrantLock必須在finally控制塊中進行解鎖操作。通常使用方式如下所示:
Lock lock = new ReentrantLock();
try {
lock.lock();
//…進行任務操作5
}finally {
lock.unlock();
}
0x03:Semaphore
上述兩種鎖機制類型都是“互斥鎖”,學過操作系統的都知道,互斥是進程同步關系的一種特殊情況,相當于只存在一個臨界資源,因此同時最多只能給一個線程提供服務。但是,在實際複雜的多線程應用程序中,可能存在多個臨界資源,這時候我們可以借助Semaphore信號量來完成多個臨界資源的訪問。
Semaphore基本能完成ReentrantLock的所有工作,使用方法也與之類似,通過acquire()與release()方法來獲得和釋放臨界資源。
經實測,Semaphone.acquire()方法默認爲可響應中斷鎖,與ReentrantLock.lockInterruptibly()作用效果一致,也就是說在等待臨界資源的過程中可以被Thread.interrupt()方法中斷。
此外,Semaphore也實現了可輪詢的鎖請求與定時鎖的功能,除了方法名tryAcquire與tryLock不同,其使用方法與ReentrantLock幾乎一致。Semaphore也提供了公平與非公平鎖的機制,也可在構造函數中進行設定。
Semaphore的鎖釋放操作也由手動進行,因此與ReentrantLock一樣,爲避免線程因抛出異常而無法正常釋放鎖的情況發生,釋放鎖的操作也必須在finally代碼塊中完成。
用于獲取權限的acquire(),其底層實現與CountDownLatch.countdown()類似;用于釋放權限的release(),其底層實現與acquire()是一個互逆的過程。
0x04:CountDownLatch
CountDownLatch是一個計數器閉鎖,通過它可以完成類似于阻塞當前線程的功能,即:一個線程或多個線程一直等待,直到其他線程執行的操作完成。CountDownLatch用一個給定的計數器來初始化,該計數器的操作是原子操作,即同時只能有一個線程去操作該計數器。調用該類await方法的線程會一直處于阻塞狀態,直到其他線程調用countDown方法使當前計數器的值變爲零,每次調用countDown計數器的值減1。當計數器值減至零時,所有因調用await()方法而處于等待狀態的線程就會繼續往下執行。這種現象只會出現一次,因爲計數器不能被重置,如果業務上需要一個可以重置計數次數的版本,可以考慮使用CycliBarrier。
在某些業務場景中,程序執行需要等待某個條件完成後才能繼續執行後續的操作;典型的應用如並行計算,當某個處理的運算量很大時,可以將該運算任務拆分成多個子任務,等待所有的子任務都完成之後,父任務再拿到所有子任務的運算結果進行彙總。
0x05:CyclicBarrier
CyclicBarrier也是一個同步輔助類,它允許一組線程相互等待,直到到達某個公共屏障點(common barrier point)。通過它可以完成多個線程之間相互等待,只有當每個線程都准備就緒後,才能各自繼續往下執行後面的操作。類似于CountDownLatch,它也是通過計數器來實現的。當某個線程調用await方法時,該線程進入等待狀態,且計數器加1,當計數器的值達到設置的初始值時,所有因調用await進入等待狀態的線程被喚醒,繼續執行後續操作。因爲CycliBarrier在釋放等待線程後可以重用,所以稱爲循環barrier。CycliBarrier支持一個可選的Runnable,在計數器的值到達設定值後(但在釋放所有線程之前),該Runnable運行一次,注,Runnable在每個屏障點只運行一個。
使用場景類似于CountDownLatch與CountDownLatch的區別
- CountDownLatch主要是實現了1個或N個線程需要等待其他線程完成某項操作之後才能繼續往下執行操作,描述的是1個線程或N個線程等待其他線程的關系。CyclicBarrier主要是實現了多個線程之間相互等待,直到所有的線程都滿足了條件之後各自才能繼續執行後續的操作,描述的多個線程內部相互等待的關系。
- CountDownLatch是一次性的,而CyclicBarrier則可以被重置而重複使用。