鎖類型
悲觀鎖
事事皆總作最壞的打算,每次去拿數據的時候都認爲別人會修改,所以每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖(共享資源每次只給一個線程使用,其它線程阻塞,用完後再把資源轉讓給其它線程)。傳統的關係型數據庫裏邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖
等,都是在做操作之前先上鎖。Java中synchronized
和ReentrantLock
等獨佔鎖就是悲觀鎖思想的實現。
樂觀鎖
事事皆總作最好的打算,每次去拿數據的時候都認爲別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可以使用版本號機制和CAS算法實現。樂觀鎖適用於多讀的應用類型,這樣可以提高吞吐量,像數據庫提供的類似於write_condition
機制,其實都是提供的樂觀鎖。在Java中 java.util.concurrent.atomic
包下面的原子變量類就是使用了樂觀鎖的一種實現方式CAS實現的。
應用場景
樂觀鎖適用於寫比較少的情況下(多讀場景),即衝突真的很少發生的時候,這樣可以省去了鎖的開銷,加大了系統的整個吞吐量。但如果是多寫的情況,一般會經常產生衝突,這就會導致上層應用會不斷的進行retry,這樣反倒是降低了性能,所以一般多寫的場景下用悲觀鎖就比較合適。
常見鎖算法
樂觀鎖常見算法
CAS算法
cas 鎖(compare and swap) 對比交換
- CAS算法涉及到三個操作數
- 需要讀寫的內存值V
- 進行比較的值 A
- 擬寫入的新值 B
當且僅當 V 的值等於 A時,CAS通過原子方式用新值B來更新V的值,否則不會執行任何操作(比較和替換是一個原子操作)。一般情況下是一個自旋操作,即不斷的重試
- 問題
- ABA 問題:
如果一個變量V初次讀取的時候是A值,並且在準備賦值的時候檢查到它仍然是A值,那我們就能說明它的值沒有被其他線程修改過了嗎?很明顯是不能的,因爲在這段時間它的值可能被改爲其他值,然後又改回A,那CAS操作就會誤認爲它從來沒有被修改過。這個問題被稱爲CAS操作的 "ABA"問題。
JDK 1.5 以後的 AtomicStampedReference類就提供了此種能力,其中的 compareAndSet方法就是首先檢查當前引用是否等於預期引用,並且當前標誌是否等於預期標誌,如果全部相等,則以原子方式將該引用和該標誌的值設置爲給定的更新值。 - 循環時間長開銷大:
自旋CAS(也就是不成功就一直循環執行直到成功)如果長時間不成功,會給CPU帶來非常大的執行開銷。 如果JVM能支持處理器提供的pause指令那麼效率會有一定的提升,pause指令有兩個作用,第一它可以延遲流水線執行指令(de-pipeline),使CPU不會消耗過多的執行資源,延遲的時間取決於具體實現的版本,在一些處理器上延遲時間是零。第二它可以避免在退出循環的時候因內存順序衝突(memory order violation)而引起CPU流水線被清空(CPU pipeline flush),從而提高CPU的執行效率 - 只能保證一個共享變量的原子操作:
CAS 只對單個共享變量有效,當操作涉及跨多個共享變量時 CAS 無效。但是從 JDK 1.5開始,提供了 AtomicReference類來保證引用對象之間的原子性,你可以把多個變量放在一個對象裏來進行 CAS 操作.所以我們可以使用鎖或者利用 AtomicReference類把多個共享變量合併成一個共享變量來操作。
- ABA 問題:
版本號算法
一般是在數據表中加上一個數據版本號version字段,表示數據被修改的次數,當數據被修改時,version值會加一。當線程A要更新數據值時,在讀取數據的同時也會讀取version值,在提交更新時,若剛纔讀取到的version值爲當前數據庫中的version值相等時才更新,否則重試更新操作,直到更新成功。
##常見悲觀鎖
Java Synchronized
synchronized是Java中的關鍵字,是一種同步鎖。可修飾實例方法,靜態方法,代碼塊。
ADD
什麼是自旋鎖?
自旋鎖(spinlock):是指當一個線程在獲取鎖的時候,如果鎖已經被其它線程獲取,那麼該線程將循環等待,然後不斷的判斷鎖是否能夠被成功獲取,直到獲取到鎖纔會退出循環。 即樂觀鎖的一種實現。
自旋鎖存在的問題
- 如果某個線程持有鎖的時間過長,就會導致其它等待獲取鎖的線程進入循環等待,消耗CPU。使用不當會造成CPU使用率極高。
- 無法滿足等待時間最長的線程優先獲取鎖。不公平的鎖就會存在“線程飢餓”問題。
自旋鎖的優點
- 自旋鎖不會使線程狀態發生切換,一直處於用戶態,即線程一直都是active的;不會使線程進入阻塞狀態,減少了不必要的上下文切換,執行速度快
- 非自旋鎖在獲取不到鎖的時候會進入阻塞狀態,從而進入內核態,當獲取到鎖的時候需要從內核態恢復,需要線程上下文切換。 (線程被阻塞後便進入內核(Linux)調度狀態,這個會導致系統在用戶態與內核態之間來回切換,嚴重影響鎖的性能)
自旋鎖的變種
MCS鎖
MCS自旋鎖是一種基於單向鏈表的高性能、公平的自旋鎖,申請加鎖的線程只需要在本地變量上自旋,直接前驅負責通知其結束自旋,從而極大地減少了不必要的處理器緩存同步的次數,降低了總線和內存的開銷。
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
public class MCSLockV2 {
/**
* MCS鎖節點
*/
public static class MCSNodeV2 {
/**
* 後繼節點
*/
volatile MCSNodeV2 next;
/**
* 默認狀態爲等待鎖
*/
volatile boolean blocked = true;
}
/**
* 線程到節點的映射
*/
private ThreadLocal<MCSNodeV2> currentThreadNode = new ThreadLocal<>();
/**
* 指向最後一個申請鎖的MCSNode
*/
volatile MCSNodeV2 queue;
/**
* 原子更新器
*/
private static final AtomicReferenceFieldUpdater UPDATER = AtomicReferenceFieldUpdater.newUpdater(
MCSLockV2.class,
MCSLockV2.MCSNodeV2.class,
"queue");
/**
* MCS獲取鎖操作
*/
public void lock() {
MCSNodeV2 cNode = currentThreadNode.get();
if (cNode == null) {
System.out.println(String.format("Thread %s 初始化 ", Thread.currentThread().getId()));
// 初始化節點對象
cNode = new MCSNodeV2();
currentThreadNode.set(cNode);
}
// 將當前申請鎖的線程置爲queue並返回舊值
MCSNodeV2 mcsNodeV2 = (MCSNodeV2) UPDATER.getAndSet(this, cNode); // step 1
if (mcsNodeV2 != null) {
// 形成鏈表結構(單向)
mcsNodeV2.next = cNode; // step 2
// 當前線程處於等待狀態時自旋(MCSNode的blocked初始化爲true)
// 等待前驅節點主動通知,即將blocked設置爲false,表示當前線程可以獲取到鎖
while (cNode.blocked) {
}
} else {
// 只有一個線程在使用鎖,沒有前驅來通知它,所以得自己標記自己爲非阻塞 - 表示已經加鎖成功
cNode.blocked = false;
}
}
/**
* MCS釋放鎖操作
*/
public void unlock() {
// 獲取當前線程對應的節點
MCSNodeV2 cNode = currentThreadNode.get();
if (cNode == null || cNode.blocked) {
// 當前線程對應存在節點
// 並且
// 鎖擁有者進行釋放鎖纔有意義 - 當blocked未true時,表示此線程處於等待狀態中,並沒有獲取到鎖,因此沒有權利釋放鎖
return;
}
if (cNode.next == null && !UPDATER.compareAndSet(this, cNode, null)) {
// 沒有後繼節點的情況,將queue置爲空
// 如果CAS操作失敗了表示突然有節點排在自己後面了,可能還不知道是誰,下面是等待後續者
// 這裏之所以要忙等是因爲上述的lock操作中step 1執行完後,step 2可能還沒執行完
while (cNode.next == null) {
}
}
if (cNode.next != null) {
// 通知後繼節點可以獲取鎖
cNode.next.blocked = false;
// 將當前節點從鏈表中斷開,方便對當前節點進行GC
cNode.next = null; // for GC
}
// 清空當前線程對應的節點信息
currentThreadNode.remove();
}
/**
* 測試用例
*
* @param args
*/
public static void main(String[] args) {
final MCSLockV2 lock = new MCSLockV2();
for (int i = 1; i <= 10; i++) {
new Thread(generateTask(lock, String.valueOf(i))).start();
}
}
private static Runnable generateTask(final MCSLockV2 lock, final String taskId) {
return () -> {
lock.lock();
try {
Thread.sleep(3000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(String.format("Thread %s Completed %s ", Thread.currentThread().getId(), taskId));
lock.unlock();
};
}
}
- MCS鎖的節點對象需要有兩個狀態,next用來維護單向鏈表的結構,blocked用來表示節點的狀態,true表示處於自旋中;false表示加鎖成功
- MCS鎖的節點狀態blocked的改變是由其前驅節點觸發改變的
- 加鎖時會更新鏈表的末節點並完成鏈表結構的維護
- 釋放鎖的時候由於鏈表結構建立的時滯(getAndSet原子方法和鏈表建立整體而言並非原子性),可能存在多線程的干擾,需要使用忙等待保證鏈表結構就緒
CLH鎖
同MCS自旋鎖一樣,CLH也是一種基於單向鏈表(隱式創建)的高性能、公平的自旋鎖,申請加鎖的線程只需要在其前驅節點的本地變量上自旋,從而極大地減少了不必要的處理器緩存同步的次數,降低了總線和內存的開銷。
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
public class CLHLockV2 {
/**
* CLH鎖節點狀態 - 每個希望獲取鎖的線程都被封裝爲一個節點對象
*/
private static class CLHNodeV2 {
/**
* 默認狀態爲true - 即處於等待狀態或者加鎖成功(換言之,即此節點處於有效的一種狀態)
*/
volatile boolean active = true;
}
/**
* 隱式鏈表最末等待節點
*/
private volatile CLHNodeV2 tail = null;
/**
* 線程對應CLH節點映射
*/
private ThreadLocal<CLHNodeV2> currentThreadCacheNode = new ThreadLocal<>();
/**
* 原子更新器
*/
private static final AtomicReferenceFieldUpdater<CLHLockV2, CLHNodeV2> UPDATER = AtomicReferenceFieldUpdater.newUpdater(CLHLockV2.class, CLHNodeV2.class, "tail");
/**
* CLH加鎖
*/
public void lock() {
CLHNodeV2 cNode = currentThreadCacheNode.get();
if (cNode == null) {
cNode = new CLHNodeV2();
currentThreadCacheNode.set(cNode);
}
// 通過這個操作完成隱式鏈表的維護,後繼節點只需要在前驅節點的locked狀態上自旋
CLHNodeV2 clhNodeV2 = UPDATER.getAndSet(this, cNode);
if (clhNodeV2 != null) {
// 自旋等待前驅節點狀態變更 - unlock中進行變更
while (clhNodeV2.active) {
//等待unlock 時被釋放
}
/**
* 因爲unlock 後 當前線程已經結束 ,所以顯示的是 阻塞線程的的下一線程的ID
*
*/
System.out.println(String.format("釋放鎖%s成功", Thread.currentThread().getId()));
}
// 沒有前驅節點表示可以直接獲取到鎖,由於默認獲取鎖狀態爲true,此時可以什麼操作都不執行
// 能夠執行到這裏表示已經成功獲取到了鎖
}
/**
* CLH釋放鎖
*/
public void unlock() {
CLHNodeV2 cNode = currentThreadCacheNode.get();
// 只有持有鎖的線程才能夠釋放
if (cNode == null || !cNode.active) {
return;
}
// 從映射關係中移除當前線程對應的節點
currentThreadCacheNode.remove();
// 嘗試將tail從currentThread變更爲null,因此當tail不爲currentThread時表示還有線程在等待加鎖
if (!UPDATER.compareAndSet(this, cNode, null)) {
// 不僅只有當前線程,還有後續節點線程的情況 - 將當前線程的鎖狀態置爲false,因此其後繼節點的lock自旋操作可以退出
cNode.active = false;
}
}
/**
* 用例
*
* @param args
*/
public static void main(String[] args) {
final CLHLockV2 lock = new CLHLockV2();
for (int i = 1; i <= 10; i++) {
new Thread(generateTask(lock, String.valueOf(i))).start();
}
}
private static Runnable generateTask(final CLHLockV2 lock, final String taskId) {
return () -> {
lock.lock();
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(String.format("Thread %s Completed %s ", Thread.currentThread().getId(), taskId));
lock.unlock();
};
}
}