共享鎖和排它鎖(ReentrantReadWriteLock)
1、什麼是共享鎖和排它鎖
共享鎖就是允許多個線程同時獲取一個鎖,一個鎖可以同時被多個線程擁有。
排它鎖,也稱作獨佔鎖,一個鎖在某一時刻只能被一個線程佔有,其它線程必須等待鎖被釋放之後纔可能獲取到鎖。
2、排它鎖和共享鎖實例
ReentrantLock就是一種排它鎖。CountDownLatch是一種共享鎖。這兩類都是單純的一類,即,要麼是排它鎖,要麼是共享鎖。
ReentrantReadWriteLock是同時包含排它鎖和共享鎖特性的一種鎖,這裏主要以ReentrantReadWriteLock爲例來進行分析學習。我們使用ReentrantReadWriteLock的寫鎖時,使用的便是排它鎖的特性;使用ReentrantReadWriteLock的讀鎖時,使用的便是共享鎖的特性。
3、鎖的等待隊列組成
ReentrantReadWriteLock有一個讀鎖(ReadLock)和一個寫鎖(WriteLock)屬性,分別代表可重入讀寫鎖的讀鎖和寫鎖。有一個Sync屬性來表示這個鎖上的等待隊列。ReadLock和WriteLock各自也分別有一個Sync屬性表示在這個鎖上的隊列
通過構造函數來看,
public ReentrantReadWriteLock(boolean fair)
{
sync = (fair)? new FairSync()
: new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
在創建讀鎖和寫鎖對象的時候,會把這個可重入的讀寫鎖上的Sync屬性傳遞過去。
protected ReadLock(ReentrantReadWriteLock
lock) {
sync = lock.sync;
}
protected WriteLock(ReentrantReadWriteLock
lock) {
sync = lock.sync;
}
所以,最終的效果是讀鎖和寫鎖使用的是同一個線程等待隊列。這個隊列就是通過我們在前面介紹過的AbstractQueuedSynchronizer實現的。
4、鎖的狀態
既然讀鎖和寫鎖使用的是同一個等待隊列,那麼這裏要如何區分一個鎖的讀狀態(有多少個線程正在讀這個鎖)和寫狀態(是否被加了寫鎖,哪個線程正在寫這個鎖)。
首先每個鎖都有一個exclusiveOwnerThread屬性,這是繼承自AbstractQueuedSynchronizer,來表示當前擁有這個鎖的線程。那麼,剩下的主要問題就是確定,有多少個線程正在讀這個鎖,以及是否加了寫鎖。
這裏可以通過線程獲取鎖時執行的邏輯來看,下面是線程獲取讀鎖時會執行的一部分代碼。
final boolean tryReadLock()
{
Thread current = Thread.currentThread();
for (;;)
{
int c
= getState();
if (exclusiveCount(c)
!= 0 &&
getExclusiveOwnerThread() != current)
return false ;
if (sharedCount(c)
== MAX_COUNT)
throw new Error("Maximum
lock count exceeded" );
if (compareAndSetState(c,
c + SHARED_UNIT)) {
HoldCounter rh = cachedHoldCounter;
if (rh
== null || rh.tid != current.getId())
cachedHoldCounter = rh = readHolds.get();
rh.count++;
return true ;
}
}
}
注意這個函數的調用exclusiveCount(c) ,用來計算這個鎖當前的寫加鎖次數(同一個進程多次進入會累加)。代碼如下
/** 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; }
相關常量的定義如下
static final int SHARED_SHIFT
= 16;
static final int EXCLUSIVE_MASK =
(1 << SHARED_SHIFT) - 1;
如果從二進制來看EXCLUSIVE_MASK的表示,這個值的低16位全是1,而高16位則全是0,所以exclusiveCount是把state的低16位取出來,表示當前這個鎖的寫鎖加鎖次數。
再來看sharedCount,取出了state的高16位,用來表示這個鎖的讀鎖加鎖次數。所以,這裏是用state的高16位和低16位來分別表示這個鎖上的讀鎖和寫鎖的加鎖次數。
現在再回頭來看tryReadLock實現,首先檢查這個鎖上是否被加了寫鎖,同時檢查加寫鎖的是不是當前線程。如果不是被當前線程加了寫鎖,那麼試圖加讀鎖就失敗了。如果沒有被加寫鎖,或者是被當前線程加了寫鎖,那麼就把讀鎖加鎖次數加1,通過compareAndSetState(c,
c + SHARED_UNIT)來實現
SHARED_UNIT的定義如下,剛好實現了高16位的加1操作。
static final int SHARED_UNIT
= (1 << SHARED_SHIFT);
5、線程阻塞和喚醒的時機
線程的阻塞和訪問其他鎖的時機相似,在線程視圖獲取鎖,但這個鎖又被其它線程佔領無法獲取成功時,線程就會進入這個鎖對象的等待隊列中,並且線程被阻塞,等待前面線程釋放鎖時被喚醒。
但因爲加讀鎖和加寫鎖進入等待隊列時存在一定的區別,加讀鎖時,final Node
node = addWaiter(Node.SHARED);節點的nextWaiter指向一個共享節點,表明當前這個線程是處於共享狀態進入等待隊列。
加寫鎖時如下,
public final void acquire (int arg)
{
if (!tryAcquire(arg)
&&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
線程是處於排它狀態進入等待隊列的。
在線程的阻塞上,讀鎖和寫鎖的時機相似,但在線程的喚醒上,讀鎖和寫鎖則存在較大的差別。
讀鎖通過AbstractQueuedSynchronizer的doAcquireShared來完成獲取鎖的動作。
private void doAcquireShared( int arg)
{
final Node
node = addWaiter(Node.SHARED);
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();
return ;
}
}
if (shouldParkAfterFailedAcquire(p,
node) &&
parkAndCheckInterrupt())
interrupted = true ;
}
} catch (RuntimeException
ex) {
cancelAcquire(node);
throw ex;
}
}
在tryAcquireShared獲取讀鎖成功後(返回正數表示獲取成功),有一個setHeadAndPropagate的函數調用。
寫鎖通過AbstractQueuedSynchronizer的acquire來實現鎖的獲取動作。
public final void acquire( int arg)
{
if (!tryAcquire(arg)
&&
acquireQueued(addWaiter(Node.EXCLUSIVE),
arg))
selfInterrupt();
}
如果tryAcquire獲取成功則直接返回,否則把線程加入到鎖的等待隊列中。和一般意義上的ReentrantLock的原理一樣。
所以在加鎖上,主要的差別在於這個setHeadAndPropagate方法,其代碼如下
private void setHeadAndPropagate (Node
node, int propagate) {
Node h = head; // Record old head for check
below
setHead(node);
/*
* Try to signal next queued node if:
* Propagation was indicated by caller,
* or was recorded (as h.waitStatus) by a previous operation
* (note: this uses sign-check of waitStatus because
* PROPAGATE status may transition to SIGNAL.)
* and
* The next node is waiting in shared mode,
* or we don't know, because it appears null
*
* The conservatism in both of these checks may cause
* unnecessary wake-ups, but only when there are multiple
* racing acquires/releases, so most need signals now or soon
* anyway.
*/
if (propagate
> 0 || h == null || h.waitStatus < 0)
{
Node s = node.next;
if (s
== null || s.isShared())
doReleaseShared();
}
}
主要操作是把這個節點設爲頭節點(成爲頭節點,則表示不在等待隊列中,因爲獲取鎖成功了),同時釋放鎖(doReleaseShared)。
下面來看doReleaseShared的實現
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 ;
}
}
把頭節點的waitStatus這隻爲0或者Node.PROPAGATE,並且喚醒下一個線程,然後就結束了。
總結一下,就是一個線程在獲取讀鎖後,會喚醒鎖的等待隊列中的第一個線程。如果這個被喚醒的線程是在獲取讀鎖時被阻塞的,那麼被喚醒後,就會在for循環中,又執行到setHeadAndPropagate,這樣就實現了讀鎖獲取時的傳遞喚醒。這種傳遞在遇到一個因爲獲取寫鎖被阻塞的線程節點時被終止。
下面通過代碼來理解這種等待和線程喚醒順序。
package lynn.lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class TestThread extends Thread
{
private ReentrantReadWriteLock lock;
private String threadName;
private boolean isWriter ;
public TestThread(ReentrantReadWriteLock
lock, String name, boolean isWriter)
{
this.lock =
lock;
this.threadName =
name;
this.isWriter =
isWriter;
}
@Override
public void run()
{
while (true )
{
try {
if (isWriter )
{
lock.writeLock().lock();
} else {
lock.readLock().lock();
}
if (isWriter )
{
Thread. sleep(3000);
System. out.println("----------------------------" );
}
System. out.println(System.currentTimeMillis()
+ ":" + threadName );
if (isWriter )
{
Thread. sleep(3000);
System. out.println("-----------------------------" );
}
} catch (Exception
e) {
e.printStackTrace();
} finally {
if (isWriter )
{
lock.writeLock().unlock();
} else {
lock.readLock().unlock();
}
}
break;
}
}
}
TestThread是一個自定義的線程類,在生成線程的時候,需要傳遞一個可重入的讀寫鎖對象進去,線程在執行時會先加鎖,然後進行內容輸出,然後釋放鎖。如果傳遞的是寫鎖,那線程在輸出結果前後會先沉睡3秒,便於區分輸出的結果時間。
package lynn.lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class Main
{
public static void blockByWriteLock()
{
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
lock.writeLock().lock();
TestThread[] threads = new TestThread[10];
for (int i
= 0; i < 10; i++) {
boolean isWriter
= (i + 1) % 4 == 0 ? true : false;
TestThread thread = new TestThread(lock, "thread-" +
(i + 1), isWriter);
threads[i] = thread;
}
for (int i
= 0; i < threads.length; i++) {
threads[i].start();
}
System. out.println(System.currentTimeMillis()
+ ": block by write lock");
try {
Thread. sleep(3000);
} catch (Exception
e) {
e.printStackTrace();
}
lock.writeLock().unlock();
}
public static void main(String[]
args) {
blockByWriteLock();
}
}
在Main中構造了10個線程,由於這個鎖一開始是被主線程擁有,並且是在排它狀態下加鎖的,所以我們構造的10個線程,在一開始執行便是按照其編號從小到大在等待隊列中(1到10)。然後主線程打印結果,等待3秒後釋放鎖。由於前3個線程,編號1到3是處於共享狀態阻塞的,而第4個線程是處於排它狀態阻塞,所以,按照上面的喚醒順序,喚醒傳遞到第4個線程時就結束。
依次類推,理論上的打印順序是 :主線程 [1,2,3] 4 [5,6,7] 8 [9,10]
從下面的執行結果來看,也是符合我們的預期的。
6、讀線程之間的喚醒
如果一個線程在共享模式下獲取了鎖狀態,這個時候,它是否要喚醒其它在共享模式下等待在該鎖上的線程?
由於多個線程可以同時獲取共享鎖而不相互影響,所以,當一個線程在共享狀態下獲取了鎖之後,理論上是可以喚醒其它在共享狀態下等待該鎖的線程。但如果這個時候,在這個等待隊列中,既有共享狀態的線程,同時又有排它狀態的線程,這個時候又該如何喚醒?
實際上對於鎖來說,在共享狀態下,一個線程無論是獲取還是釋放鎖的時候,都會試着去喚醒下一個等待在這個鎖上的節點(通過上面的doAcquireShared代碼能看出)。如果下一個線程也是處於共享狀態等待在鎖上,那麼這個線程就會被喚醒,然後接着試着去喚醒下一個等待在這個鎖上的線程,這種喚醒動作會一直持續下去,直到遇到一個在排它狀態下阻塞在這個鎖上的線程,或者等待隊列全部被釋放爲止。
因爲線程是在一個FIFO的等待隊列中,所以,這這樣一個一個往後傳遞,就能保證喚醒被傳遞下去。
參考資料 http://www.liuinsect.com/2014/09/04/jdk1-8-abstractqueuedsynchronizer-part2/
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.