獨享還是共享,你選擇哪一種鎖?
之前在的文章中已經寫了公平鎖和非公平鎖了,接下來就該介紹第二種鎖了,他就是共享鎖和獨享鎖,顧名思義,獨享,只能被一個執行緒 所持有,而共享,就是說可以被多個執行緒所共有。
鎖的分類
1.公平鎖/非公平鎖
2.可重入鎖3.獨享鎖/共享鎖4.互斥鎖/讀寫鎖5.樂觀鎖/悲觀鎖6.分段鎖7.偏向鎖/輕量級鎖/重量級鎖8.自旋鎖
之前的第一次分享中我們已經說過了公平鎖和非公平鎖了,這次我們組要來解析一下這個獨享鎖和共享鎖。
獨享鎖
獨享鎖其實有很多名稱的,有人稱它為獨享鎖,有人也稱它為獨佔鎖,其實大致上都是一個意思,
獨享鎖,只能夠被一個執行緒所持有,
而他的例項我們之前的公平鎖和非公平鎖也都說過一次,我們可以再看一下這個例項,
ReentrantLock(獨享)
ReentrantLock是基於AQS來實現的,那什麼是AQS呢?
AQS全稱AbstractQueuedSynchronizer,如果說使用翻譯軟體來看“摘要排隊同步器”,但是很多人喜歡稱它為抽象佇列同步器。 其實叫什麼倒是沒有那麼重要,只要記住英文,這才是最重要的。
AQS它定義了一套多執行緒訪問共享資源的同步器框架,很多類都是依賴於AQS來比如說我們一會將要介紹的ReentrantLock。
你看原始碼
/*
查詢是否有任何執行緒正在等待與此鎖相關聯的給定條件。
請注意,由於超時和*中斷可能隨時發生,
此方法主要用於監視系統狀態
*/
public boolean hasWaiters(Condition condition) {
if (condition == null)
throw new NullPointerException();
if (!(condition instanceof AbstractQueuedSynchronizer.ConditionObject))
throw new IllegalArgumentException("not owner");
return sync.hasWaiters((AbstractQueuedSynchronizer.ConditionObject)condition);
}
這裡就指明瞭我們說的ReentrantLock是依賴AQS的,而AQS它是JUC併發包中的一個核心的一個元件。 也是不可或缺的元件。
AQS解決了子啊實現同步器時涉及當的大量細節問題,例如獲取同步狀態、FIFO同步佇列。基於AQS來構建同步器可以帶來很多好處。它不僅能夠極大地減少實現工作,而且也不必處理在多個位置上發生的競爭問題。
在基於AQS構建的同步器中,只能在一個時刻發生阻塞,從而降低上下文切換的開銷,提高了吞吐量。
AQS的主要使用方式是繼承,子類通過繼承同步器並實現它的抽象方法來管理同步狀態。
咱們可以看一下
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
private static final long serialVersionUID = 7373984972572414691L;
而它典型的例子ReentrantLock中:
使用一個int型別的成員變數state來表示同步狀態,當state>0時表示已經獲取了鎖
這就是我們之前看的int c = getState();
而當c等於0的時候說明當前沒有執行緒佔有鎖,它提供了三個方法(getState()、setState(int newState)、compareAndSetState(int expect,int update))來對同步狀態state進行操作,所以AQS可以確保對state的操作是安全的。
關於AQS我就解釋這麼多把,如果想深入瞭解的可以仔細的研究一下,而在這個ReentrantLock中的原始碼是這樣的
/**
它預設是非公平鎖
*/
public ReentrantLock() {
sync = new NonfairSync();
}
/**
建立ReentrantLock,公平鎖or非公平鎖
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
/**
而他會分別呼叫lock方法和unlock方法來釋放鎖
*/
public void lock() {
sync.lock();
}
public void unlock() {
sync.release(1);
}
但是其實他不僅僅是會呼叫lock和unlock方法,因為我們的執行緒不可能一點問題沒有,如果說進入到了waiting狀態,在這個時候如果沒有unpark()方法,就沒有辦法來喚醒他, 所以,也就接踵而至出現了tryLock(),tryLock(long,TimeUnit)來做一些嘗試加鎖或者說是超市來滿足某些特定的場景的需求了。
ReentrantLock會保證method-body在同一時間只有一個執行緒在執行這段程式碼,或者說,同一時刻只有一個執行緒的lock方法會返回。其餘執行緒會被掛起,直到獲取鎖。
從這裡我們就能看出,其實ReentrantLock實現的就是一個獨佔鎖的功能:有且只有一個執行緒獲取到鎖,其餘執行緒全部掛起,直到該擁有鎖的執行緒釋放鎖,被掛起的執行緒被喚醒重新開始競爭鎖。
而在原始碼中通過AQS來獲取獨享鎖是通過呼叫acquire方法,其實這個方法是阻塞的,
/**
*以獨佔模式獲取,忽略中斷。通過至少呼叫tryAcquire實現
成功返回。否則執行緒排隊,可能重複阻塞和解除阻塞,
呼叫tryAcquire直到成功。
此方法可用於實現方法lock。
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
它通過tryAcquire(由子類Sync實現)嘗試獲取鎖,這也是上面原始碼中的lock方法的實現步驟
而沒有獲取到鎖則呼叫AQS的acquireQueued方法:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
這段的意思大致就是說 當前驅節點是頭節點,並且獨佔時才返回
而在下面的if判斷中,他會去進行阻塞,而且還要去判斷是否打斷,如果我們的節點狀態是Node.SIGNAL時, 完蛋了,執行緒將會執行parkAndCheckInterrupt方法,知道有執行緒release的時候,這時候就會進行一個unpark來迴圈的去獲取鎖。 而這個方法通過LockSupport.park(this)將當前的執行緒掛起到WATING的狀態,就需要我們去執行unpark方法了來喚醒他,也就是我說的那個release, 通過這樣的一種FIFO機制的等待就實現了LOCK的操作。
這上面的程式碼只是進行加鎖,但是沒有釋放鎖,如果說我們獲得了鎖不進行釋放,那麼很自然的出現一種情況,死鎖!
所以必須要進行一個釋放,
我們來看看內部是怎麼釋放鎖的
public void unlock() { sync.release(1); }
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
unlock方法間接呼叫AQS的release(1)來完成釋放
tryRelease(int)方法進行了特殊的判定,如果成立則會將head傳入unparkSuccessor(Node) 方法中並且返回true,否則返回的就是false。
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
而他在執行了unparkSuccessor方法中的時候,就已經意味著要真正的釋放鎖了。 這其實就是獨享鎖進行獲取鎖和釋放鎖的一個過程!有興趣的可以去原始碼中把註釋翻譯一下看看。
共享鎖
從我們之前的獨享所就能看得出來,獨享鎖是使用的一個狀態來進行鎖標記的,共享鎖其實也差不多,但是JAVA中有不想定力兩個狀態,所以區別出現了, 他們的鎖狀態時不一樣的。
基本的流程是一樣的,主要區別在於判斷鎖獲取的條件上,由於是共享鎖,也就允許多個執行緒同時獲取,所以同步狀態的數量同時的大於1的,如果同步狀態為非0,則執行緒就可以獲取鎖,只有當同步狀態為0時,才說明共享數量的鎖已經被全部獲取,其餘執行緒只能等待。
最典型的就是ReentrantReadWriteLock裡的讀鎖,它的讀鎖是可以被共享的,但是它的寫鎖確每次只能被獨佔。
我們來看一下他的獲取鎖和釋放鎖的程式碼體現。
//獲取鎖指定離不開這個lock方法,
public void lock() {
sync.acquireShared(1);
}
//acquireShared()首先會通過tryAcquireShared()來嘗試獲取鎖。
//如果說獲取不到那麼他就回去執行 doAcquireShared(arg);直到獲取到鎖才會返回
//你看方法名do是不是想到了do-while呢?
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
// tryAcquireShared()來嘗試獲取鎖。
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
//只有這個方法獲取到鎖了才會進行返回
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
//上面的這些方法全部都是在AbstractQueuedSynchronizer中
//而他通過Sync來呼叫的acquireShared
//而Sync則是繼承的AbstractQueuedSynchronizer
abstract static class Sync extends AbstractQueuedSynchronizer
而他呼叫的tryAcquireShared則是在ReentrantReadWriteLock中
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
//獲取狀態
int c = getState();
//如果說鎖狀態不是0 並且獲取鎖的執行緒不是current執行緒 返回-1
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
//統計讀鎖的次數
int r = sharedCount(c);
//若無需等待,並且共享讀鎖共享次數小於MAX_COUNT,則會把鎖的共享次數加一,
//否則他會去執行fullTryAcquireShared
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current);
}
/** fullTryAcquireShared()會根據是否需要阻塞等待
讀取鎖的共享計數是否超過限制”等等進行處理。
如果不需要阻塞等待,並且鎖的共享計數沒有超過限制,
則通過CAS嘗試獲取鎖,並返回1。*/
final int fullTryAcquireShared(Thread current) {
/*
* This code is in part redundant with that in
* tryAcquireShared but is simpler overall by not
* complicating tryAcquireShared with interactions between
* retries and lazily reading hold counts.
*/
HoldCounter rh = null;
for (;;) {
int c = getState();
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
// else we hold the exclusive lock; blocking here
// would cause deadlock.
} else if (readerShouldBlock()) {
// Make sure we're not acquiring read lock reentrantly
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
} else {
if (rh == null) {
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) {
rh = readHolds.get();
if (rh.count == 0)
readHolds.remove();
}
}
if (rh.count == 0)
return -1;
}
}
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
if (compareAndSetState(c, c + SHARED_UNIT)) {
if (sharedCount(c) == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
if (rh == null)
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
cachedHoldCounter = rh; // cache for release
}
return 1;
}
}
}
以上的原始碼就是共享鎖的一個獲取鎖的過程
接下來肯定是要進行鎖的釋放了
unlock()
public void unlock() {
sync.releaseShared(1);
}
//和獲取鎖的過程類似,他首先會通過tryReleaseShared()去嘗試釋放共享鎖。嘗試成功,則直接返回;嘗試失敗,
//則通過doReleaseShared()去釋放共享鎖。
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
//是嘗試釋放共享鎖第一步。
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
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;
}
}
//持續執行釋放共享鎖
private void doReleaseShared() {
/*
* Ensure that a release propagates, even if there are other
* in-progress acquires/releases. This proceeds in the usual
* way of trying to unparkSuccessor of head if it needs
* signal. But if it does not, status is set to PROPAGATE to
* ensure that upon release, propagation continues.
* Additionally, we must loop in case a new node is added
* while we are doing this. Also, unlike other uses of
* unparkSuccessor, we need to know if CAS to reset status
* fails, if so rechecking.
*/
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
以上的程式碼就是共享鎖和非共享鎖的原始碼。需要注意的時候,在這裡其實很亂,有些方法是定義在ReentrantReadWriteLock中的, 而有一些方法是定義在AbstractQueuedSynchorizer中的,所以在來回切換看程式碼的時候尤其要注意,不要出現失誤。
總結
獨享鎖:同時只能有一個執行緒獲得鎖。
共享鎖:可以有多個執行緒同時獲得鎖。
關於獨享鎖和共享鎖,你明白了嗎?
Java 極客技術公眾號,是由一群熱愛 Java 開發的技術人組建成立,專注分享原創、高質量的 Java 文章。如果您覺得我們的文章還不錯,請幫忙讚賞、在看、轉發支援,鼓勵我們分享出更好的文章。 關注公眾號,大家可以在公眾號後臺回覆“部落格園”,免費獲得作者 Java 知識體系/面試必看資料。
&n