大夥在面試的時候應該會經常碰到線程
併發
方面的問題,而且也會問到你各種分佈式鎖的概念,本文就給大家整理了下各種鎖的分類,希望對你有所幫助。
鎖的分類
我們先總體的來看看有哪些具體的分類
序號 | 分類 |
---|---|
1 | 樂觀鎖/悲觀鎖 |
2 | 獨享鎖/共享鎖 |
3 | 互斥鎖/讀寫鎖 |
4 | 可重入鎖 |
5 | 公平鎖/非公平鎖 |
6 | 分段鎖 |
7 | 偏向鎖/輕量級鎖/重量級鎖 |
8 | 自旋鎖 |
一、樂觀鎖/悲觀鎖
注意,樂觀鎖
與悲觀鎖
並不是具體的兩種鎖的實現,而一種設計思想。
樂觀鎖
顧名思義,就是很樂觀,每次去拿數據的時候都認爲別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可以使用版本號
等機制。
樂觀鎖適用於多讀
的應用類型,這樣可以提高吞吐量,不加鎖會帶來大量的性能提升,在Java中java.util.concurrent.atomic
包下面的原子變量類就是使用了樂觀鎖的一種實現方式CAS(Compare and Swap 比較並交換)實現的。
實現方式
數據版本機制
實現數據版本一般有兩方式,
- 第一種是使用版本號
- 第二種是使用時間戳。
以版本號方式爲例。
版本號方式:一般是在數據表中加上一個數據版本號version字段,表示數據被修改的次數,當數據被修改時,version值會加一。當線程A要更新數據值時,在讀取數據的同時也會讀取version值,在提交更新時,若剛纔讀取到的version值爲當前數據庫中的version值相等時才更新,否則重試更新操作,直到更新成功。
update table set xxx=#{xxx}, version=version+1 where id=#{id} and version=#{version};
CAS操作
CAS
(Compare and Swap 比較並交換),當多個線程嘗試使用CAS同時更新同一個變量時,只有其中一個線程能更新變量的值,而其它線程都失敗,失敗的線程並不會被掛起,而是被告知這次競爭中失敗,並可以再次嘗試。
CAS操作中包含三個操作數——需要讀寫的內存位置(V)、進行比較的預期原值(A)和擬寫入的新值(B)。如果內存位置V的值與預期原值A相匹配,那麼處理器會自動將該位置值更新爲新值B,否則處理器不做任何操作。
悲觀鎖
總是假設最壞的情況,每次去拿數據的時候都認爲別人會修改,所以每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖。比如Java裏面的同步原語synchronized關鍵字的實現就是悲觀鎖。
悲觀鎖適合寫
操作非常多的場景,保證數據的安全。
二、獨享鎖/共享鎖
獨享鎖
是指該鎖一次只能被一個線程所持有。
共享鎖
是指該鎖可被多個線程所持有。
ReentrantLock
ReentrantLock是獨享鎖。但是對於Lock的另一個實現類ReadWriteLock,其讀鎖是共享鎖,其寫鎖是獨享鎖。
ReadWriteLock
ReadWriteLock,其讀鎖
是共享鎖,其寫鎖是
獨享鎖。讀鎖的共享鎖可保證併發讀是非常高效的,讀寫,寫讀,寫寫的過程是互斥的。
Synchronized
Synchronized是獨享鎖
獨享鎖與共享鎖也是通過AQS
來實現的,通過實現不同的方法,來實現獨享或者共享。
三、互斥鎖/讀寫鎖
上面講的獨享鎖/共享鎖就是一種廣義的說法,互斥鎖/讀寫鎖就是具體的實現。
互斥鎖
在Java中的具體實現就是ReentrantLock。
讀寫鎖
在Java中的具體實現就是ReadWriteLock。
四、可重入鎖
可重入鎖又名遞歸鎖
,是指在同一個線程在外層方法獲取鎖的時候,在進入內層方法會自動獲取鎖。說的有點抽象,下面會有一個代碼的示例。
ReetrantLock
ReetrantLock從名字就可以看出是一個重入鎖,其名字是Re entrant Lock 重新進入鎖。
Synchronized
對於Synchronized而言,也是一個可重入鎖。可重入鎖的一個好處是可一定程度避免死鎖。
public class AtomicDemo {
// 共享操作的資源 volatile 原子性問題 synchroized Lock AutomicInteger
private static int count = 0 ;
private static Lock lock = new ReentrantLock();
// 定義操作的方法 重入鎖
private static void incr(){
try {
lock.lock();// 枷鎖
Thread.sleep(1);
count++;
decr();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
private static void decr(){
try {
lock.lock();// 枷鎖
Thread.sleep(1);
count--;
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i <100 ; i++) {
new Thread(new Runnable() {
public void run() {
AtomicDemo.incr();
}
}).start();
}
Thread.sleep(5000);
System.out.println("Result:" + count);
}
}
五、公平鎖/非公平鎖
公平鎖
是指多個線程按照申請鎖的順序來獲取鎖。
非公平鎖
是指多個線程獲取鎖的順序並不是按照申請鎖的順序,有可能後申請的線程比先申請的線程優先獲取鎖。有可能,會造成優先級反轉或者飢餓現象。
ReetrantLock
ReetrantLock可以通過構造函數指定該鎖是否是公平鎖,默認是非公平鎖。非公平鎖的優點在於吞吐量比公平鎖大。
Synchronized
對於Synchronized而言,也是一種非公平鎖。由於其並不像ReentrantLock是通過AQS的來實現線程調度,所以並沒有任何辦法使其變成公平鎖。
六、分段鎖
分段鎖
其實是一種鎖的設計,並不是具體的一種鎖,對於ConcurrentHashMap
而言,其併發的實現就是通過分段鎖的形式來實現高效的併發操作。
我們以ConcurrentHashMap來說一下分段鎖的含義以及設計思想,ConcurrentHashMap中的分段鎖稱爲Segment,它即類似於HashMap(JDK7和JDK8中HashMap的實現)的結構,即內部擁有一個Entry數組,數組中的每個元素又是一個鏈表;同時又是一個ReentrantLock(Segment繼承了ReentrantLock)。
當需要put元素的時候,並不是對整個hashmap進行加鎖,而是先通過hashcode來知道他要放在哪一個分段中,然後對這個分段進行加鎖,所以當多線程put的時候,只要不是放在一個分段中,就實現了真正的並行的插入。
但是,在統計size的時候,可就是獲取hashmap全局信息的時候,就需要獲取所有的分段鎖才能統計。
分段鎖的設計目的是細化鎖
的粒度,當操作不需要更新整個數組的時候,就僅僅針對數組中的一項進行加鎖操作
七、偏向鎖/輕量級鎖/重量級鎖
這三種鎖是指鎖的狀態,並且是針對Synchronized。在Java 5通過引入鎖升級的機制來實現高效Synchronized。這三種鎖的狀態是通過對象監視器在對象頭中的字段來表明的。
偏向鎖
是指一段同步代碼一直被一個線程所訪問,那麼該線程會自動獲取鎖。降低獲取鎖的代價。
輕量級鎖
是指當鎖是偏向鎖的時候,被另一個線程所訪問,偏向鎖就會升級爲輕量級鎖,其他線程會通過自旋的形式嘗試獲取鎖,不會阻塞,提高性能。
重量級鎖
是指當鎖爲輕量級鎖的時候,另一個線程雖然是自旋,但自旋不會一直持續下去,當自旋一定次數的時候,還沒有獲取到鎖,就會進入阻塞,該鎖膨脹爲重量級鎖。重量級鎖會讓他申請的線程進入阻塞,性能降低。
八、自旋鎖
在Java中,自旋鎖是指嘗試獲取鎖的線程不會立即阻塞,而是採用循環的方式去嘗試獲取鎖,這樣的好處是減少線程上下文切換的消耗,缺點是循環會消耗CPU。
好了~鎖的分類就給大夥介紹到這裏,有問題的歡迎大家留言交流!!!