首頁 > 軟體

面試頻繁被問Java中的各種鎖,今天就給大夥來梳理梳理,建議收藏

2021-05-30 17:00:53

大夥在面試的時候應該會經常碰到執行緒 併發方面的問題,而且也會問到你各種分散式鎖的概念,本文就給大家整理了下各種鎖的分類,希望對你有所幫助。

鎖的分類

我們先總體的來看看有哪些具體的分類

一、樂觀鎖/悲觀鎖

注意,樂觀鎖與悲觀鎖並不是具體的兩種鎖的實現,而一種設計思想。

樂觀鎖

顧名思義,就是很樂觀,每次去拿資料的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個資料,可以使用版本號等機制。

樂觀鎖適用於多讀的應用類型,這樣可以提高吞吐量,不加鎖會帶來大量的效能提升,在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。

最後,懇請大家點贊+關注!


IT145.com E-mail:sddin#qq.com