在Java併發場景中,會涉及到各類各樣的鎖如公平鎖,樂觀鎖,悲觀鎖等等,這篇文章介紹各類鎖的分類:java
公平鎖/非公平鎖mysql
可重入鎖web
獨享鎖/共享鎖算法
樂觀鎖/悲觀鎖sql
分段鎖數據庫
自旋鎖編程
線程鎖數組
樂觀鎖與悲觀鎖是一種廣義上的概念,體現了看待線程同步的不一樣角度,在Java和數據庫中都有此概念對應的實際應用。緩存
1.樂觀鎖安全
顧名思義,就是很樂觀,每次去拿數據的時候都認爲別人不會修改,因此不會上鎖,可是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可使用版本號等機制。
樂觀鎖適用於多讀的應用類型,樂觀鎖在Java中是經過使用無鎖編程來實現,最常採用的是CAS算法,Java原子類中的遞增操做就經過CAS自旋實現的。
CAS全稱 Compare And Swap(比較與交換),是一種無鎖算法。在不使用鎖(沒有線程被阻塞)的狀況下實現多線程之間的變量同步。java.util.concurrent包中的原子類就是經過CAS來實現了樂觀鎖。
簡單來講,CAS算法有3個三個操做數:
當且僅當預期值A和內存值V相同時,將內存值V修改成B,不然返回V。這是一種樂觀鎖的思路,它相信在它修改以前,沒有其它線程去修改它;而Synchronized是一種悲觀鎖,它認爲在它修改以前,必定會有其它線程去修改它,悲觀鎖效率很低。
2.悲觀鎖
老是假設最壞的狀況,每次去拿數據的時候都認爲別人會修改,因此每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖。
傳統的MySQL關係型數據庫裏邊就用到了不少這種鎖機制,好比行鎖,表鎖等,讀鎖,寫鎖等,都是在作操做以前先上鎖。
相對其餘數據庫而言,MySQL的鎖機制比較簡單,其最顯著的特色是不一樣的存儲引擎支持不一樣的鎖機制。
好比:
MySQL主要的兩種鎖的特性可大體概括以下:
1.主要是針對鎖粒度劃分的,通常分爲:行鎖、表鎖、庫鎖
(1)行鎖:訪問數據庫的時候,鎖定整個行數據,防止併發錯誤。
(2)表鎖:訪問數據庫的時候,鎖定整個表數據,防止併發錯誤。
2.行鎖 和 表鎖 的區別:
(1)悲觀鎖:顧名思義,就是很悲觀,每次去拿數據的時候都認爲別人會修改,因此每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會block直到它拿到鎖。
傳統的關係型數據庫裏邊就用到了不少這種鎖機制,好比行鎖,表鎖等,讀鎖,寫鎖等,都是在作操做以前先上鎖。
(2)樂觀鎖: 顧名思義,就是很樂觀,每次去拿數據的時候都認爲別人不會修改,因此不會上鎖,可是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可使用版本號等機制。
樂觀鎖適用於多讀的應用類型,這樣能夠提升吞吐量,像數據庫若是提供相似於write_condition機制的其實都是提供的樂觀鎖。
(3)悲觀鎖 和 樂觀鎖的區別:
兩種鎖各有優缺點,不可認爲一種好於另外一種,像樂觀鎖適用於寫比較少的狀況下,即衝突真的不多發生的時候,這樣能夠省去了鎖的開銷,加大了系統的整個吞吐量。但若是常常產生衝突,上層應用會不斷的進行retry,這樣反卻是下降了性能,因此這種狀況下用悲觀鎖就比較合適。
共享鎖指的就是對於多個不一樣的事務,對同一個資源共享同一個鎖。至關於對於同一把門,它擁有多個鑰匙同樣。就像這樣,你家有一個大門,大門的鑰匙有好幾把,你有一把,你女友有一把,大家均可能經過這把鑰匙進入大家家,這個就是所謂的共享鎖。
剛剛說了,對於悲觀鎖,通常數據庫已經實現了,共享鎖也屬於悲觀鎖的一種,那麼共享鎖在mysql中是經過什麼命令來調用呢。經過查詢資料,瞭解到經過在執行語句後面加上lock in share mode就表明對某些資源加上共享鎖了。
何時使用表鎖
對於InnoDB表,在絕大部分狀況下都應該使用行級鎖,由於事務和行鎖每每是咱們之因此選擇InnoDB表的理由。但在個別特殊事務中,也能夠考慮使用表級鎖。
固然,應用中這兩種事務不能太多,不然,就應該考慮使用MyISAM表了。
再好比上面提到的Java的同步synchronized關鍵字的實現就是典型的悲觀鎖。
3.總之:
1.公平鎖
就是很公平,在併發環境中,每一個線程在獲取鎖時會先查看此鎖維護的等待隊列,若是爲空,或者當前線程是等待隊列的第一個,就佔有鎖,不然就會加入到等待隊列中,之後會按照FIFO的規則從隊列中取到本身。
公平鎖的優勢是等待鎖的線程不會餓死。缺點是總體吞吐效率相對非公平鎖要低,等待隊列中除第一個線程之外的全部線程都會阻塞,CPU喚醒阻塞線程的開銷比非公平鎖大。
2.非公平鎖
上來就直接嘗試佔有鎖,若是嘗試失敗,就再採用相似公平鎖那種方式。
非公平鎖的優勢是能夠減小喚起線程的開銷,總體的吞吐效率高,由於線程有概率不阻塞直接得到鎖,CPU沒必要喚醒全部線程。缺點是處於等待隊列中的線程可能會餓死,或者等好久纔會得到鎖。
3.典型應用:
java jdk併發包中的ReentrantLock能夠指定構造函數的boolean類型來建立公平鎖和非公平鎖(默認),好比:公平鎖可使用new ReentrantLock(true)實現。
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併發編程中,常常遇到多個線程訪問同一個 共享資源 ,這時候做爲開發者必須考慮如何維護數據一致性,這就是Java鎖機制(同步問題)的來源。
Java提供了多種多線程鎖機制的實現方式,常見的有:
- synchronized
- ReentrantLock
- Semaphore
- AtomicInteger等
每種機制都有優缺點與各自的適用場景,必須熟練掌握他們的特色才能在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、AtomicLong等,他們的實現原理相同,區別在與運算對象類型的不一樣。
咱們知道,在多線程程序中,諸如++i
或
i++等運算不具備原子性,是不安全的線程操做之一。一般咱們會使用synchronized將該操做變成一個原子操做,但JVM爲此類操做特地提供了一些同步類,使得使用更方便,且使程序運行效率變得更高。經過相關資料顯示,一般AtomicInteger的性能是ReentantLock的好幾倍。
1.synchronized:
在資源競爭不是很激烈的狀況下,偶爾會有同步的情形下,synchronized是很合適的。緣由在於,編譯程序一般會盡量的進行優化synchronize,另外可讀性很是好。
2.ReentrantLock:
在資源競爭不激烈的情形下,性能稍微比synchronized差點點。可是當同步很是激烈的時候,synchronized的性能一會兒能降低好幾十倍,而ReentrantLock確還能維持常態。
高併發量狀況下使用ReentrantLock。
3.Atomic:
和上面的相似,不激烈狀況下,性能比synchronized略遜,而激烈的時候,也能維持常態。激烈的時候,Atomic的性能會優於ReentrantLock一倍左右。可是其有一個缺點,就是隻能同步一個值,一段代碼中只能出現一個Atomic的變量,多於一個同步無效。由於他不能在多個Atomic之間同步。
因此,咱們寫同步的時候,優先考慮synchronized,若是有特殊須要,再進一步優化。ReentrantLock和Atomic若是用的很差,不只不能提升性能,還可能帶來災難。
以上就是Java線程鎖的詳解,除了從編程的角度應對高併發,更多還須要從架構設計的層面來應對高併發場景,例如:Redis緩存、CDN、異步消息等,詳細的內容以下。