高併發程式設計系列:4種Java執行緒鎖全面詳解

張哥說技術發表於2023-04-23

併發程式設計現在已經是必備技能了,之前談了併發程式設計:4大併發工具類(圖文詳解),本篇接著談併發程式設計裡非常重要的4大併發鎖


為了弄清楚執行緒鎖,我先回頭鎖的源頭,這樣更有利理解鎖的本質。



01

為什麼需要多執行緒


使用多執行緒的理由之一是和程式相比,它是一種非常花銷小,切換快,更"節儉"的多工操作方式。


在Linux系統下,啟動一個新的程式必須分配給它獨立的地址空間,建立眾多的資料表來維護它的程式碼段、堆疊段和資料段,這是一種"昂貴"的多工工作方式。


而在程式中的同時執行多個執行緒,它們彼此之間使用相同的地址空間,共享大部分資料,啟動一個執行緒所花費的空間遠遠小於啟動一個程式所花費的空間,而且,執行緒間彼此切換所需的時間也遠遠小於程式間切換所需要的時間。


當然,多執行緒除了剛才談到的優勢,它也會面臨著部分挑戰。



02

多執行緒面臨的挑戰



高併發程式設計系列:4種Java執行緒鎖全面詳解


由於多個執行緒是共同佔有所屬程式的資源和地址空間的,那麼就會存在一個問題:


如果多個執行緒要同時訪問某個資源,怎麼處理?


在Java併發程式設計中,經常遇到多個執行緒訪問同一個 共享資源 ,這時候作為開發者必須考慮如何維護資料一致性,這就是Java鎖機制(執行緒同步)的來源


Java提供了多種多執行緒鎖機制的實現方式,常見的有:

  1. synchronized

  2. ReentrantLock

  3. Semaphore

  4. AtomicInteger等


每種機制都有優缺點與各自的適用場景,必須熟練掌握他們的特點才能在Java多執行緒應用開發時得心應手。


03

4種Java執行緒鎖


高併發程式設計系列:4種Java執行緒鎖全面詳解


1.synchronized


在Java中synchronized關鍵字被常用於維護資料一致性。


synchronized機制是給共享資源上鎖,只有拿到鎖的執行緒才可以訪問共享資源,這樣就可以強制使得對共享資源的訪問都是順序的。


Java開發人員都認識synchronized,使用它來實現多執行緒的同步操作是非常簡單的,只要在需要同步的對方的方法、類或程式碼塊中加入該關鍵字,它能夠保證在同一個時刻最多隻有一個執行緒執行同一個物件的同步程式碼,可保證修飾的程式碼在執行過程中不會被其他執行緒干擾。


使用synchronized修飾的程式碼具有原子性和可見性,在需要程式同步的程式中使用的頻率非常高,可以滿足一般的程式同步要求。


  1. 原子性:原子,即一個不可再被分割的顆粒。在Java中原子性指的是一個或多個操作要麼全部執行成功要麼全部執行失敗。


  2. 有序性:程式執行的順序按照程式碼的先後順序執行。(處理器可能會對指令進行重排序)


  3. 可見性:當多個執行緒訪問同一個變數時,如果其中一個執行緒對其作了修改,其他執行緒能立即獲取到最新的值。


synchronized實現的機理依賴於軟體層面上的JVM,因此其效能會隨著Java版本的不斷升級而提高。


到了Java1.6,synchronized進行了很多的最佳化,有適應自旋、鎖消除、鎖粗化、輕量級鎖及偏向鎖等,效率有了本質上的提高。在之後推出的Java1.7與1.8中,均對該關鍵字的實現機理做了最佳化。


需要說明的是,當執行緒透過synchronized等待鎖時是不能被Thread.interrupt()中斷的,因此程式設計時必須檢查確保合理,否則可能會造成執行緒死鎖的尷尬境地。


最後,儘管Java實現的鎖機制有很多種,並且有些鎖機制效能也比synchronized高,但還是強烈推薦在多執行緒應用程式中使用該關鍵字,因為實現方便,後續工作由JVM來完成,可靠性高。只有在確定鎖機制是當前多執行緒程式的效能瓶頸時,才考慮使用其他機制,如ReentrantLock等。


2.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();

}


3.Semaphore


上述兩種鎖機制型別都是“互斥鎖”,學過作業系統的都知道,互斥是程式同步關係的一種特殊情況,相當於只存在一個臨界資源,因此同時最多隻能給一個執行緒提供服務。但是,在實際複雜的多執行緒應用程式中,可能存在多個臨界資源,這時候我們可以藉助Semaphore訊號量來完成多個臨界資源的訪問。


Semaphore基本能完成ReentrantLock的所有工作,使用方法也與之類似,透過acquire()與release()方法來獲得和釋放臨界資源。


經實測,Semaphone.acquire()方法預設為可響應中斷鎖,與ReentrantLock.lockInterruptibly()作用效果一致,也就是說在等待臨界資源的過程中可以被Thread.interrupt()方法中斷。


此外,Semaphore也實現了可輪詢的鎖請求與定時鎖的功能,除了方法名tryAcquire與tryLock不同,其使用方法與ReentrantLock幾乎一致。Semaphore也提供了公平與非公平鎖的機制,也可在建構函式中進行設定。


Semaphore的鎖釋放操作也由手動進行,因此與ReentrantLock一樣,為避免執行緒因丟擲異常而無法正常釋放鎖的情況發生,釋放鎖的操作也必須在finally程式碼塊中完成


4.AtomicInteger


首先說明,此處AtomicInteger是一系列相同類的代表之一,常見的還有AtomicLong、AtomicLong等,他們的實現原理相同,區別在與運算物件型別的不同。


我們知道,在多執行緒程式中,諸如++i 或 i++等運算不具有原子性,是不安全的執行緒操作之一。通常我們會使用synchronized將該操作變成一個原子操作,但JVM為此類操作特意提供了一些同步類,使得使用更方便,且使程式執行效率變得更高。透過相關資料顯示,通常AtomicInteger的效能是ReentantLock的好幾倍。


04

Java執行緒鎖總結


1.synchronized


在資源競爭不是很激烈的情況下,偶爾會有同步的情形下,synchronized是很合適的。原因在於,編譯程式通常會盡可能的進行最佳化synchronize,另外可讀性非常好。


2.ReentrantLock


在資源競爭不激烈的情形下,效能稍微比synchronized差點點。但是當同步非常激烈的時候,synchronized的效能一下子能下降好幾十倍,而ReentrantLock確還能維持常態。


3.Atomic


不激烈情況下,效能比synchronized略遜,而激烈的時候,也能維持常態。激烈的時候,Atomic的效能會優於ReentrantLock一倍左右。


但是其有一個缺點,就是隻能同步一個值,一段程式碼中只能出現一個Atomic的變數,多於一個同步無效,因為他不能在多個Atomic之間同步。


所以,我們寫同步的時候,優先考慮synchronized,如果有特殊需要,再進一步最佳化。ReentrantLock和Atomic如果用的不好,不僅不能提高效能,還可能帶來災難。


併發程式設計除了從程式設計的角度應對高併發,更多還需要從架構設計的層面來應對高併發場景,例如:Redis快取、CDN、非同步訊息等。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024923/viewspace-2947844/,如需轉載,請註明出處,否則將追究法律責任。

相關文章