獨享鎖/共享鎖+公平鎖/非公平鎖+樂觀鎖/悲觀鎖
本文標題:最全Java鎖詳解:獨享鎖/共享鎖+公平鎖/非公平鎖+樂觀鎖/悲觀鎖
轉載請保留頁面地址:http://youzhixueyuan.com/detailed-explanation-of-java-lock.html
在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關係型資料庫裡邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。詳情可以參考:
再比如上面提到的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全域性資訊的時候,就需要獲取所有的分段鎖才能統計。
分段鎖的設計目的是細化鎖的粒度,當操作不需要更新整個陣列的時候,就僅僅針對陣列中的一項進行加鎖操作。