鎖的分類#
可重入 / 不可重入鎖#
參考鏈接: 究竟什麼是可重入鎖
廣義上的可重入鎖指的是可重複可遞歸調用的鎖,在外層使用鎖之後,在內層仍然可以使用,並且不發生死鎖(前提得是同一個對象或者 class)
在 Java 中ReentrantLock
和synchronized
都是可重入鎖,區別在於:
Synchronized
是依賴於 JVM 實現的,而ReentrantLock
是 JDK 實現的ReentrantLock
可以指定是公平鎖還是非公平鎖。而Synchronized
只能是非公平鎖
ReentrantLock 中可重入鎖實現#
實現原理:在 AQS 中維護了一個 private volatile int state 來計數重入次數,避免了頻繁的持有釋放操作,這樣既提升了效率,又避免了死鎖
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 實現原理
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
// setState是AbstractQueuedSynchronizer中final定義的不可變方法
setState(nextc);
return true;
}
return false;
}
公平鎖 / 非公平鎖#
公平鎖就是保障了多線程下各線程獲取鎖的順序,先到的線程優先獲取鎖 (通過維護一個FIFO隊列實現
),而非公平鎖則無法提供這個保障,ReentrantLock
、ReadWriteLock
默認都是非公平模式,因為非公平鎖減少了線程掛起的機率,後來的線程有一定機率逃離被掛起的開銷。 結合代碼來看:
//非公平鎖
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//區別重點看這裡
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
//公平鎖
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//hasQueuedPredecessors這個方法就是最大區別所在
// 在獲取鎖之前會先判斷等待隊列是否為空或者自己是否位於隊列頭部,該條件通過才能繼續獲取鎖
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
讀寫鎖#
參考鏈接: 深入理解讀寫鎖 ReentrantReadWriteLock
讀寫鎖是怎樣實現分別記錄讀寫狀態的#
同步狀態變量 state 的高 16 位用來表示讀鎖被獲取的次數,低 16 位用來表示寫鎖的獲取次數
/** Returns the number of shared holds represented in count */
// 讀鎖是共享鎖
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
/** Returns the number of exclusive holds represented in count */
// 寫鎖是獨占鎖
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
寫鎖是怎樣獲取和釋放的#
寫鎖獲取#
寫鎖是獨占式鎖,在同一時刻寫鎖是不能被多個線程所獲取,實現寫鎖的同步語義是通過重寫 AQS 中的 tryAcquire 方法實現的:
其主要邏輯為:當讀鎖已經被讀線程獲取或者寫鎖已經被其他寫線程獲取,則寫鎖獲取失敗;否則,獲取成功並支持重入,增加寫狀態
寫鎖釋放#
寫鎖釋放通過重寫 AQS 的 tryRelease 方法
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//1. 同步狀態減去寫狀態
int nextc = getState() - releases;
//2. 當前寫狀態是否為0,為0則釋放寫鎖
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
//3. 不為0則更新同步狀態
setState(nextc);
return free;
}
讀鎖是怎樣獲取和釋放的#
讀鎖的獲取#
當寫鎖被其他線程獲取後,讀鎖獲取失敗,否則獲取成功利用 CAS 更新同步狀態。另外,當前同步狀態需要加上 SHARED_UNIT, 原因是同步狀態的高 16 位用來表示讀鎖被獲取的次數。如果 CAS 失敗或者已經獲取讀鎖的線程再次獲取讀鎖時,是靠 fullTryAcquireShared 方法實現的
讀鎖的釋放#
讀鎖釋放的實現主要通過方法 tryReleaseShared,源碼如下:
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
// 前面還是為了實現getReadHoldCount等新功能
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;
}
for (;;) {
int c = getState();
// 讀鎖釋放 將同步狀態減去讀狀態即可
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
// Releasing the read lock has no effect on readers,
// but it may allow waiting writers to proceed if
// both read and write locks are now free.
return nextc == 0;
}
}
樂觀鎖 / 悲觀鎖#
參考鏈接: 面試必備之樂觀鎖與悲觀鎖
樂觀鎖#
總是假設最好的情況,每次去拿數據的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可以使用版本號機制和CAS算法
實現
樂觀鎖適用於多讀的應用類型,這樣可以提高吞吐量
樂觀鎖的缺點 (CAS 算法的缺陷)#
- ABA 問題 (JDK 1.5 以後的
AtomicStampedReference
類解決) - 循環時間長開銷大:自旋 CAS, 也就是不成功就一直循環執行直到成功,如果長時間不成功,會給 CPU 帶來非常大的執行開銷
- 只能保證一個共享變量的原子操作: CAS 只對單個共享變量有效,當操作涉及跨多個共享變量時 CAS 無效,從 JDK 1.5 開始,提供了
AtomicReference
類來保證引用對象之間的原子性,你可以把多個變量放在一個對象裡來進行 CAS 操作
悲觀鎖#
每次去拿數據的時候都認為別人會修改,所以每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖(共享資源每次只給一個線程使用,其它線程阻塞,用完後再把資源轉讓給其它線程)
- 傳統的關係型數據庫裡邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖
- Java 中 synchronized 和 ReentrantLock 等獨占鎖就是悲觀鎖思想的實現