1. 程式人生 > >最全Java鎖詳解:獨享鎖/共享鎖+公平鎖/非公平鎖+樂觀鎖/悲觀鎖

最全Java鎖詳解:獨享鎖/共享鎖+公平鎖/非公平鎖+樂觀鎖/悲觀鎖

最全Java鎖詳解:獨享鎖/共享鎖+公平鎖/非公平鎖+樂觀鎖/悲觀鎖


在Java併發場景中,會涉及到各種各樣的鎖如公平鎖,樂觀鎖,悲觀鎖等等,這篇文章介紹各種鎖的分類:
公平鎖/非公平鎖
可重入鎖
獨享鎖/共享鎖
樂觀鎖/悲觀鎖
分段鎖
自旋鎖

樂觀鎖 VS 悲觀鎖

樂觀鎖與悲觀鎖是一種廣義上的概念,體現了看待執行緒同步的不同角度,在Java和資料庫中都有此概念對應的實際應用。

1.樂觀鎖

顧名思義,就是很樂觀,每次去拿資料的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個資料,可以使用版本號等機制。
   樂觀鎖適用於多讀的應用型別,樂觀鎖在Java中是通過使用無鎖程式設計來實現,最常採用的是CAS演算法,Java原子類中的遞增操作就通過CAS自旋實現的。
   CAS全稱 Compare And Swap(比較與交換),是一種無鎖演算法。在不使用鎖(沒有執行緒被阻塞)的情況下實現多執行緒之間的變數同步。java.util.concurrent包中的原子類就是通過CAS來實現了樂觀鎖。
   簡單來說,CAS演算法有3個三個運算元:
   需要讀寫的記憶體值 V。
   進行比較的值 A。
   要寫入的新值 B。
   且僅當預期值A和記憶體值V相同時,將記憶體值V修改為B,否則返回V。這是一種樂觀鎖的思路,它相信在它修改之前,沒有其它執行緒去修改它;而Synchronized是一種悲觀鎖,它認為在它修改之前,一定會有其它執行緒去修改它,悲觀鎖效率很低。

2.悲觀鎖

總是假設最壞的情況,每次去拿資料的時候都認為別人會修改,所以每次在拿資料的時候都會上鎖,這樣別人想拿這個資料就會阻塞直到它拿到鎖。
   傳統的MySQL關係型資料庫裡邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。詳情可以參考:阿里P8架構師談:MySQL行鎖、表鎖、悲觀鎖、樂觀鎖的特點與應用
   再比如上面提到的Java的同步synchronized關鍵字的實現就是典型的悲觀鎖。
   在這裡插入圖片描述

3.總之

悲觀鎖適合寫操作多的場景,先加鎖可以保證寫操作時資料正確。
   樂觀鎖適合讀操作多的場景,不加鎖的特點能夠使其讀操作的效能大幅提升。

公平鎖 VS 非公平鎖

1.公平鎖

就是很公平,在併發環境中,每個執行緒在獲取鎖時會先檢視此鎖維護的等待佇列,如果為空,或者當前執行緒是等待佇列的第一個,就佔有鎖,否則就會加入到等待佇列中,以後會按照FIFO的規則從佇列中取到自己。
公平鎖的優點是等待鎖的執行緒不會餓死。缺點是整體吞吐效率相對非公平鎖要低,等待佇列中除第一個執行緒以外的所有執行緒都會阻塞,CPU喚醒阻塞執行緒的開銷比非公平鎖大。

2.非公平鎖

上來就直接嘗試佔有鎖,如果嘗試失敗,就再採用類似公平鎖那種方式。
   非公平鎖的優點是可以減少喚起執行緒的開銷,整體的吞吐效率高,因為執行緒有機率不阻塞直接獲得鎖,CPU不必喚醒所有執行緒。缺點是處於等待佇列中的執行緒可能會餓死,或者等很久才會獲得鎖。

3.典型應用

java jdk併發包中的ReentrantLock可以指定建構函式的boolean型別來建立公平鎖和非公平鎖(預設),比如:公平鎖可以使用new ReentrantLock(true)實現。

獨享鎖 VS 共享鎖

1.獨享鎖

是指該鎖一次只能被一個執行緒所持有。

2.共享鎖

是指該鎖可被多個執行緒所持有。

3.比較

對於Java ReentrantLock而言,其是獨享鎖。但是對於Lock的另一個實現類ReadWriteLock,其讀鎖是共享鎖,其寫鎖是獨享鎖。
   讀鎖的共享鎖可保證併發讀是非常高效的,讀寫,寫讀 ,寫寫的過程是互斥的。
   獨享鎖與共享鎖也是通過AQS來實現的,通過實現不同的方法,來實現獨享或者共享。

4.AQS

抽象佇列同步器(AbstractQueuedSynchronizer,簡稱AQS)是用來構建鎖或者其他同步元件的基礎框架,它使用一個整型的volatile變數(命名為state)來維護同步狀態,通過內建的FIFO佇列來完成資源獲取執行緒的排隊工作。
   在這裡插入圖片描述
   concurrent包的實現結構如上圖所示,AQS、非阻塞資料結構和原子變數類等基礎類都是基於volatile變數的讀/寫和CAS實現,而像Lock、同步器、阻塞佇列、Executor和併發容器等高層類又是基於基礎類實現。

分段鎖

分段鎖其實是一種鎖的設計,並不是具體的一種鎖,對於ConcurrentHashMap而言,其併發的實現就是通過分段鎖的形式來實現高效的併發操作。
   我們以ConcurrentHashMap來說一下分段鎖的含義以及設計思想,ConcurrentHashMap中的分段鎖稱為Segment,它即類似於HashMap(JDK7與JDK8中HashMap的實現)的結構,即內部擁有一個Entry陣列,陣列中的每個元素又是一個連結串列;同時又是一個ReentrantLock(Segment繼承了ReentrantLock)。
   當需要put元素的時候,並不是對整個hashmap進行加鎖,而是先通過hashcode來知道他要放在那一個分段中,然後對這個分段進行加鎖,所以當多執行緒put的時候,只要不是放在一個分段中,就實現了真正的並行的插入。
   但是,在統計size的時候,可就是獲取hashmap全域性資訊的時候,就需要獲取所有的分段鎖才能統計。
   分段鎖的設計目的是細化鎖的粒度,當操作不需要更新整個陣列的時候,就僅僅針對陣列中的一項進行加鎖操作。

Java執行緒鎖

多執行緒的緣由

在出現了程序之後,作業系統的效能得到了大大的提升。雖然程序的出現解決了作業系統的併發問題,但是人們仍然不滿足,人們逐漸對實時性有了要求。
   使用多執行緒的理由之一是和程序相比,它是一種非常花銷小,切換快,更”節儉”的多工操作方式。
在Linux系統下,啟動一個新的程序必須分配給它獨立的地址空間,建立眾多的資料表來維護它的程式碼段、堆疊段和資料段,這是一種”昂貴”的多工工作方式。而在程序中的同時執行多個執行緒,它們彼此之間使用相同的地址空間,共享大部分資料,啟動一個執行緒所花費的空間遠遠小於啟動一個程序所花費的空間,而且,執行緒間彼此切換所需的時間也遠遠小於程序間切換所需要的時間。

多執行緒併發面臨的問題

由於多個執行緒是共同佔有所屬程序的資源和地址空間的,那麼就會存在一個問題:如果多個執行緒要同時訪問某個資源,怎麼處理?
   在Java併發程式設計中,經常遇到多個執行緒訪問同一個共享資源 ,這時候作為開發者必須考慮如何維護資料一致性,這就是Java鎖機制(同步問題)的來源。
   Java提供了多種多執行緒鎖機制的實現方式,常見的有:

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

4種Java執行緒鎖(執行緒同步)

1.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等。

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

Java執行緒鎖總結

  • 1.synchronized:
    在資源競爭不是很激烈的情況下,偶爾會有同步的情形下,synchronized是很合適的。原因在於,編譯程式通常會盡可能的進行優化synchronize,另外可讀性非常好。
  • 2.ReentrantLock:
    在資源競爭不激烈的情形下,效能稍微比synchronized差點點。但是當同步非常激烈的時候,synchronized的效能一下子能下降好幾十倍,而ReentrantLock確還能維持常態。高併發量情況下使用ReentrantLock。
  • 3.Atomic:
    和上面的類似,不激烈情況下,效能比synchronized略遜,而激烈的時候,也能維持常態。激烈的時候,Atomic的效能會優於ReentrantLock一倍左右。但是其有一個缺點,就是隻能同步一個值,一段程式碼中只能出現一個Atomic的變數,多於一個同步無效。因為他不能在多個Atomic之間同步。所以,我們寫同步的時候,優先考慮synchronized,如果有特殊需要,再進一步優化。ReentrantLock和Atomic如果用的不好,不僅不能提高效能,還可能帶來災難。
    以上就是Java執行緒鎖的詳解,除了從程式設計的角度應對高併發,更多還需要從架構設計的層面來應對高併發場景,例如:Redis快取、CDN、非同步訊息等。