前言
本文對Java的“共享鎖”進行介紹,JUC中的共享鎖有CountDownLatch, CyclicBarrier, Semaphore, ReentrantReadWriteLock等;本章會以ReentrantReadWriteLock爲例對共享鎖進行說明。內容包括
- ReadWriteLock 和 ReentrantReadWriteLock介紹
- ReadWriteLock 和 ReentrantReadWriteLock函數說明
- ReentrantReadWriteLock 類圖結構
- ReentrantReadWriteLock原理
- ReentrantReadWriteLock示例
ReadWriteLock 和 ReentrantReadWriteLock介紹
ReadWriteLock維護了一對相關的鎖 — — “讀取鎖”和“寫入鎖”,一個用於讀取操作,另一個用於寫入操作。
“讀取鎖”用於只讀操作,它是“共享鎖”,能同時被多個線程獲取。
“寫入鎖”用於寫入操作,它是“獨佔鎖”,寫入鎖只能被一個線程鎖獲取。
注意:不能同時存在讀取鎖和寫入鎖!
ReadWriteLock 和 ReentrantReadWriteLock函數說明
// 返回用於讀取操作的鎖。
Lock readLock()
// 返回用於寫入操作的鎖。
Lock writeLock()
// 創建一個新的 ReentrantReadWriteLock,默認是採用“非公平策略”。
ReentrantReadWriteLock()
// 創建一個新的 ReentrantReadWriteLock,fair是“公平策略”。fair爲true,意味着公平策略;否則,意味着非公平策略。
ReentrantReadWriteLock(boolean fair)
// 返回一個 collection,它包含可能正在等待獲取讀取鎖的線程。
protected Collection<Thread> getQueuedReaderThreads()
// 返回一個 collection,它包含可能正在等待獲取讀取或寫入鎖的線程。
protected Collection<Thread> getQueuedThreads()
// 返回一個 collection,它包含可能正在等待獲取寫入鎖的線程。
protected Collection<Thread> getQueuedWriterThreads()
// 返回用於讀取操作的鎖。
ReentrantReadWriteLock.ReadLock readLock()
// 返回用於寫入操作的鎖。
ReentrantReadWriteLock.WriteLock writeLock()
ReentrantReadWriteLock 類圖結構
從類圖結構看出:
- ReentrantReadWriteLock實現了ReadWriteLock接口。ReadWriteLock是一個讀寫鎖的接口,提供了"獲取讀鎖的readLock()函數" 和 “獲取寫鎖的writeLock()函數”。
- ReentrantReadWriteLock中包含:sync對象,讀鎖readerLock和寫鎖writerLock。讀鎖ReadLock和寫鎖WriteLock都實現了Lock接口。讀鎖ReadLock和寫鎖WriteLock中也都分別包含了"Sync對象",它們的Sync對象和ReentrantReadWriteLock的Sync對象 是一樣的,就是通過sync,讀鎖和寫鎖實現了對同一個對象的訪問。
- 和"ReentrantLock"一樣,sync是Sync類型;而且,Sync也是一個繼承於AQS的抽象類。Sync也包括"公平鎖"FairSync和"非公平鎖"NonfairSync。sync對象是"FairSync"和"NonfairSync"中的一個,默認是"NonfairSync"。
共享鎖源碼說明
ReadLock源碼
public static class ReadLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = -5992448646407690164L;
// ReentrantReadWriteLock的AQS對象
private final Sync sync;
protected ReadLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
// 獲取“共享鎖”
public void lock() {
sync.acquireShared(1);
}
// 如果線程是中斷狀態,則拋出一場,否則嘗試獲取共享鎖。
public void lockInterruptibly() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
// 嘗試獲取“共享鎖”
public boolean tryLock() {
return sync.tryReadLock();
}
// 在指定時間內,嘗試獲取“共享鎖”
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
// 釋放“共享鎖”
public void unlock() {
sync.releaseShared(1);
}
// 新建條件
public Condition newCondition() {
throw new UnsupportedOperationException();
}
public String toString() {
int r = sync.getReadLockCount();
return super.toString() +
"[Read locks = " + r + "]";
}
}
下面,分別從“獲取共享鎖”和“釋放共享鎖”兩個方面對共享鎖進行說明。
獲取鎖
獲取共享鎖的思想(即lock函數的步驟),是先通過tryAcquireShared()嘗試獲取共享鎖。嘗試成功的話,則直接返回;嘗試失敗的話,則通過doAcquireShared()不斷的循環並嘗試獲取鎖,若有需要,則阻塞等待。doAcquireShared()在循環中每次嘗試獲取鎖時,都是通過tryAcquireShared()來進行嘗試的。下面看看“獲取共享鎖”的詳細流程。
- lock()
public void lock() {
sync.acquireShared(1);
}
- acquireShared()
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
說明:acquireShared()首先會通過tryAcquireShared()來嘗試獲取鎖。
嘗試成功的話,則不再做任何動作(因爲已經成功獲取到鎖了)。
嘗試失敗的話,則通過doAcquireShared()來獲取鎖。doAcquireShared()會獲取到鎖了才返回。
- tryAcquireShared()
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
// 獲取“鎖”的狀態
int c = getState();
// 如果“鎖”是“互斥鎖”,並且獲取鎖的線程不是current線程;則返回-1。
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
// 獲取“讀取鎖”的共享計數
int r = sharedCount(c);
// 如果“不需要阻塞等待”,並且“讀取鎖”的共享計數小於MAX_COUNT;
// 則通過CAS函數更新“鎖的狀態”,將“讀取鎖”的共享計數+1。
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
// 第1次獲取“讀取鎖”。
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
// 如果想要獲取鎖的線程(current)是第1個獲取鎖(firstReader)的線程
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
// HoldCounter是用來統計該線程獲取“讀取鎖”的次數。
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != current.getId())
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
// 將該線程獲取“讀取鎖”的次數+1。
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current);
}
說明:tryAcquireShared()的作用是嘗試獲取“共享鎖”。
如果在嘗試獲取鎖時,“不需要阻塞等待”並且“讀取鎖的共享計數小於MAX_COUNT”,則直接通過CAS函數更新“讀取鎖的共享計數”,以及將“當前線程獲取讀取鎖的次數+1”。
否則,通過fullTryAcquireShared()獲取讀取鎖。
- fullTryAcquireShared()
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {
// 獲取“鎖”的狀態
int c = getState();
// 如果“鎖”是“互斥鎖”,並且獲取鎖的線程不是current線程;則返回-1。
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
// 如果“需要阻塞等待”。
// (01) 當“需要阻塞等待”的線程是第1個獲取鎖的線程的話,則繼續往下執行。
// (02) 當“需要阻塞等待”的線程獲取鎖的次數=0時,則返回-1。
} else if (readerShouldBlock()) {
// 如果想要獲取鎖的線程(current)是第1個獲取鎖(firstReader)的線程
if (firstReader == current) {
} else {
if (rh == null) {
rh = cachedHoldCounter;
if (rh == null || rh.tid != current.getId()) {
rh = readHolds.get();
if (rh.count == 0)
readHolds.remove();
}
}
// 如果當前線程獲取鎖的計數=0,則返回-1。
if (rh.count == 0)
return -1;
}
}
// 如果“不需要阻塞等待”,則獲取“讀取鎖”的共享統計數;
// 如果共享統計數超過MAX_COUNT,則拋出異常。
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 將線程獲取“讀取鎖”的次數+1。
if (compareAndSetState(c, c + SHARED_UNIT)) {
// 如果是第1次獲取“讀取鎖”,則更新firstReader和firstReaderHoldCount。
if (sharedCount(c) == 0) {
firstReader = current;
firstReaderHoldCount = 1;
// 如果想要獲取鎖的線程(current)是第1個獲取鎖(firstReader)的線程,
// 則將firstReaderHoldCount+1。
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
if (rh == null)
rh = cachedHoldCounter;
if (rh == null || rh.tid != current.getId())
rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
// 更新線程的獲取“讀取鎖”的共享計數
rh.count++;
cachedHoldCounter = rh; // cache for release
}
return 1;
}
}
}
說明:fullTryAcquireShared()會根據“是否需要阻塞等待”,“讀取鎖的共享計數是否超過限制”等等進行處理。如果不需要阻塞等待,並且鎖的共享計數沒有超過限制,則通過CAS嘗試獲取鎖,並返回1。
- doAcquireShared()
private void doAcquireShared(int arg) {
// addWaiter(Node.SHARED)的作用是,創建“當前線程”對應的節點,並將該線程添加到CLH隊列中。
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 獲取“node”的前一節點
final Node p = node.predecessor();
// 如果“當前線程”是CLH隊列的表頭,則嘗試獲取共享鎖。
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
// 如果“當前線程”不是CLH隊列的表頭,則通過shouldParkAfterFailedAcquire()判斷是否需要等待,
// 需要的話,則通過parkAndCheckInterrupt()進行阻塞等待。若阻塞等待過程中,線程被中斷過,則設置interrupted爲true。
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
說明:doAcquireShared()的作用是獲取共享鎖。
它會首先創建線程對應的CLH隊列的節點,然後將該節點添加到CLH隊列中。CLH隊列是管理獲取鎖的等待線程的隊列。如果“當前線程”是CLH隊列的表頭,則嘗試獲取共享鎖;否則,則需要通過shouldParkAfterFailedAcquire()判斷是否阻塞等待,需要的話,則通過parkAndCheckInterrupt()進行阻塞等待。
doAcquireShared()會通過for循環,不斷的進行上面的操作;目的就是獲取共享鎖。需要注意的是:doAcquireShared()在每一次嘗試獲取鎖時,是通過tryAcquireShared()來執行的!
釋放鎖
釋放共享鎖的思想,是先通過tryReleaseShared()嘗試釋放共享鎖。嘗試成功的話,則通過doReleaseShared()喚醒“其他等待獲取共享鎖的線程”,並返回true;否則的話,返回flase。
- unlock
public void unlock() {
sync.releaseShared(1);
}
說明:該函數實際上調用releaseShared(1)釋放共享鎖。
- releaseShared()
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
說明:releaseShared()的目的是讓當前線程釋放它所持有的共享鎖。它首先會通過tryReleaseShared()去嘗試釋放共享鎖。嘗試成功,則直接返回;嘗試失敗,則通過doReleaseShared()去釋放共享鎖。
- tryReleaseShared()
protected final boolean tryReleaseShared(int unused) {
// 獲取當前線程,即釋放共享鎖的線程。
Thread current = Thread.currentThread();
// 如果想要釋放鎖的線程(current)是第1個獲取鎖(firstReader)的線程,
// 並且“第1個獲取鎖的線程獲取鎖的次數”=1,則設置firstReader爲null;
// 否則,將“第1個獲取鎖的線程的獲取次數”-1。
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
// 獲取rh對象,並更新“當前線程獲取鎖的信息”。
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != current.getId())
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;
}
for (;;) {
// 獲取鎖的狀態
int c = getState();
// 將鎖的獲取次數-1。
int nextc = c - SHARED_UNIT;
// 通過CAS更新鎖的狀態。
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
說明:tryReleaseShared()的作用是嘗試釋放共享鎖。
- doReleaseShared()
private void doReleaseShared() {
for (;;) {
// 獲取CLH隊列的頭節點
Node h = head;
// 如果頭節點不爲null,並且頭節點不等於tail節點。
if (h != null && h != tail) {
// 獲取頭節點對應的線程的狀態
int ws = h.waitStatus;
// 如果頭節點對應的線程是SIGNAL狀態,則意味着“頭節點的下一個節點所對應的線程”需要被unpark喚醒。
if (ws == Node.SIGNAL) {
// 設置“頭節點對應的線程狀態”爲空狀態。失敗的話,則繼續循環。
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
// 喚醒“頭節點的下一個節點所對應的線程”。
unparkSuccessor(h);
}
// 如果頭節點對應的線程是空狀態,則設置“文件點對應的線程所擁有的共享鎖”爲其它線程獲取鎖的空狀態。
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
// 如果頭節點發生變化,則繼續循環。否則,退出循環。
if (h == head) // loop if head changed
break;
}
}
說明:doReleaseShared()會釋放“共享鎖”。它會從前往後的遍歷CLH隊列,依次“喚醒”然後“執行”隊列中每個節點對應的線程;最終的目的是讓這些線程釋放它們所持有的鎖。
公平共享鎖和非公平共享鎖
和互斥鎖ReentrantLock一樣,ReadLock也分爲公平鎖和非公平鎖。
公平鎖和非公平鎖的區別,體現在判斷是否需要阻塞的函數readerShouldBlock()是不同的
- 在公平共享鎖中,如果在當前線程的前面有其他線程在等待獲取共享鎖,則返回true;否則,返回false。
- 在非公平共享鎖中,它會無視當前線程的前面是否有其他線程在等待獲取共享鎖。只要該非公平共享鎖對應的線程不爲null,則返回true。
ReentrantReadWriteLock示例
public class RWLockTest {
/**
* ReentrantReadWriterLock類提供兩把鎖,一把用於寫操作,一把用於讀操作,
* 通過在ReadWriterLock接口聲明的readLock獲取。
* 並可以使用lock(),unLock(),tryLock()對象
* 用於寫操作鎖,通過在ReadWriterLock接口聲明的writeLock獲取。
* 並可以使用lock(),unLock(),tryLock()方法,當對象獲得讀鎖時,不能去修改該對象
*/
public static void main(String[] args) {
PrincePOJO prince=new PrincePOJO();
//創建多個讀取線程對象
Reader[] reader=new Reader[5];
Thread[] threadsReader=new Thread[5];
for(int i=0;i<5;i++){
reader[i]=new Reader(prince);
threadsReader[i]=new Thread(reader[i]);
}
//創建一個寫線程對象
Thread threadWriter=new Thread(new Writer(prince));
//啓動線程
for(int i=0;i<5;i++){
threadsReader[i].start();
}
threadWriter.start();
}
static class Writer implements Runnable {
private PrincePOJO price;
public Writer(PrincePOJO price){
this.price=price;
}
//循環修改價格
@Override
public void run() {
for(int i=0;i<1;i++){
System.out.println(Thread.currentThread().getName()+":attempt to modify the price");
price.setPrice(Math.random()*10 ,Math.random()*8);
System.out.println(Thread.currentThread().getName()+":price have been modfied");
try {
Thread.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
static class Reader implements Runnable {
private PrincePOJO price ;
public Reader(PrincePOJO price){
this.price=price;
}
//讀取10次價格
@Override
public void run() {
for(int i=0;i<3;i++){
System.out.printf("%s: price1:%f-price2:%f\n",Thread.currentThread().getName(),price.getPrice1(),price.getPrice2());
}
}
}
static class PrincePOJO {
private double price1;
private double price2;
//聲明一個ReadWriteLock對象
private ReadWriteLock lock;
public PrincePOJO(){
this.price1=10.00;
this.price2=8.00;
lock=new ReentrantReadWriteLock();
}
//讀取price1
public double getPrice1(){
lock.readLock().lock();
double value=price1;
lock.readLock().unlock();
return value;
}
//讀取price2
public double getPrice2(){
lock.readLock().lock();
double value=price2;
lock.readLock().unlock();
return value;
}
//設置價格
public void setPrice(double price1,double price2){
lock.writeLock().lock();
this.price1=price1;
this.price2=price2;
lock.writeLock().unlock();
}
}
}
運行結果
Thread-1: price1:10.000000-price2:8.000000
Thread-5:attempt to modify the price
Thread-4: price1:10.000000-price2:8.000000
Thread-4: price1:10.000000-price2:8.000000
Thread-4: price1:4.354122-price2:3.533028
Thread-3: price1:10.000000-price2:8.000000
Thread-3: price1:4.354122-price2:3.533028
Thread-3: price1:4.354122-price2:3.533028
Thread-0: price1:10.000000-price2:8.000000
Thread-0: price1:4.354122-price2:3.533028
Thread-0: price1:4.354122-price2:3.533028
Thread-2: price1:10.000000-price2:8.000000
Thread-2: price1:4.354122-price2:3.533028
Thread-5:price have been modfied
Thread-1: price1:10.000000-price2:8.000000
Thread-2: price1:4.354122-price2:3.533028
Thread-1: price1:4.354122-price2:3.533028
分析說明
- 觀察Thread-0和Thread-4的運行結果,我們發現,Thread-0啓動並獲取到“讀取鎖”,在它還沒運行完畢的時候,Thread-1,2,3,4也啓動了並且也成功獲取到“讀取鎖”。
因此,“讀取鎖”支持被多個線程同時獲取。 - 觀察Thread-5這三此“寫入鎖”的線程。只要“寫入鎖”被某線程獲取,則該線程運行完畢了,才釋放該鎖。
因此,“寫入鎖”不支持被多個線程同時獲取。