## 23 按需上鎖—ReadWriteLock詳解
> 合理安排時間,就等于節約時間。
> ——培根
前文我們分析了 java.util.concurrent.locks 包下的 Lock 接口和它的實現 ReentrantLock。ReentrantLock 是一種顯式鎖,提供更為高級的功能,但需要顯式的上鎖和解鎖。ReentrantLock 也是互斥鎖,有如下三種互斥情況:讀/寫、寫/寫、讀/讀。ReentrantLock 的加鎖策略是保守的,任意操作都需要先加鎖才可以.但其實在某些情況下,我們只需要控制“讀/寫”和“寫/寫”這兩種互斥情況。一般情況下,絕大多數程序中讀操作比例更大。其實讀操作之間并不需要互斥,因為讀的需求是讀到最新的數據,并且在讀的同時不要有其它線程修改數據即可。那我們需要的鎖是“讀/寫”互斥,而“讀/讀”并不互斥。這樣的好處是,讀操作可以并發進行,減少了互斥的情況,能夠提升程序的性能。打個比方,我們去旅游,在觀景臺大家可以一塊欣賞風景,這就是 “讀/讀” 并不互斥,大家可以在觀景臺一塊看。但是假如在觀景臺有個望遠鏡,需要使用望遠鏡才能看清。那么就變成了 “讀/讀” 互斥,人多的話顯然會造成排隊現象。

顯然 ReentrantLock 和 synchronized 是做不到這一點的,本節我們會介紹一種新的鎖—讀寫鎖 ReadWriteLock。
## 1、ReadWriteLock 簡介
ReadWriteLock 為我們提供了讀寫之間不同互斥策略的鎖。因此,在某些情況下,他能夠帶來更好的性能。一般來說,假如你的程序有頻繁的讀操作,那么ReadWriteLock可能會為你帶來性能的提升。但是由于讀寫控制的策略不一樣,帶來了鎖內部的復雜度。所以如果你程序的讀操作并沒有達到一定數量,反而使用讀寫鎖會比互斥鎖性能更差。
因此 ReadWriteLock 是一種提升性能的手段,但不一定奏效。我們的程序可以嘗試使用它來調節性能,如果發現沒有效果或者更差,也可以很方便的換回互斥鎖。
ReadWriteLock 顧名思義讀寫鎖,也就是說同一個鎖對讀和寫的上鎖方式是不一樣的寫鎖的互斥性更高。這里我們來看看鎖降級和升級的概念。
**鎖降級**
如果線程持有寫鎖,如果可以在不釋放寫鎖的情況下,獲取讀鎖,這就是鎖降級。ReadWriteLock 是支持鎖降級的。
**鎖升級**
如果線程持有讀鎖,那么他是否可以不釋放讀鎖,直接獲取寫鎖。這意味著從一個低級別的鎖升級到高級別的鎖。其實就是變相的插隊,無視其它在排隊等待寫鎖的線程。ReadWriteLock 并不支持鎖升級。
以上兩種概念我們可以通過寫段代碼來體驗下。
代碼一:
~~~java
public class Client {
public static void main(String[] args) throws InterruptedException {
ReadWriteLock lock = new ReentrantReadWriteLock();
Lock readLock = lock.readLock();
Lock writeLock = lock.writeLock();
writeLock.lock();
System.out.println("got the write lock");
readLock.lock();
System.out.println("got the read lock");
}
}
~~~
輸出:
~~~java
got the write lock
got the read lock
~~~
代碼二:
~~~java
public class Client {
public static void main(String[] args) throws InterruptedException {
ReadWriteLock lock = new ReentrantReadWriteLock();
Lock readLock = lock.readLock();
Lock writeLock = lock.writeLock();
readLock.lock();
System.out.println("got the read lock");
writeLock.lock();
System.out.println("got the write lock");
}
}
~~~
輸出:
~~~java
got the read lock
~~~
第一段代碼中,我們可以在獲取寫鎖后,再次成功獲得讀鎖。而代碼 2 中,我們在獲取讀鎖后試圖去獲取寫鎖。這樣會使得程序阻塞在 readLock.lock()。由于 writeLock 沒有機會 unlock,就形成了死鎖。
## 2、ReadWriteLock使用
ReadWriteLock 使用起來其實很簡單,和 Lock 基本一致。我們使用它主要是為了對性能進行優化。我們通過下面的例子,一是熟悉它的使用,二來也可以測試下它對性能的優化效果。
首先我們看使用 Lock 的情況:
~~~java
public class LockExample {
String myName;
ReentrantLock lock = new ReentrantLock();
public void printMyName() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "My name is " + myName);
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void setMyName() {
lock.lock();
try {
myName=Thread.currentThread().getName();
System.out.println(Thread.currentThread().getName() + "set my name to " + myName);
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
Long startTime = new Date().getTime();
LockExample example = new LockExample();
new Thread(()->{
while (true){
example.setMyName();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(()->{
while (true){
example.setMyName();
try {
Thread.sleep(new Random().nextInt(10));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
IntStream.range(0,100).forEach(num->{
new Thread(()->{
example.printMyName();
System.out.println("NO. "+num+" reader finished. Time passed: "+(new Date().getTime()-startTime));
}).start();
});
}
}
~~~
以上代碼我們啟動了兩個線程不斷寫入,每次間隔 10ms, 以讓其它線程能夠獲取到鎖。另外有 100 個讀線程 ,每次讀取完成,睡眠 10ms,目的是延遲讀鎖的釋放。由于使用了排他鎖,所以讀取操作間是互斥的,每次讀取都要等 10ms 釋放鎖后,其它線程才能讀取。那么 100 次讀取就至少花費了 100\*10 = 1000ms。再加上其它消耗,所以最終全部讀取線程完成工作的時候,過去了 1182ms。輸出如下:
~~~
NO. 96 reader finished. Time passed: 1148
Thread-99My name is Thread-0
NO. 97 reader finished. Time passed: 1160
Thread-100My name is Thread-0
NO. 98 reader finished. Time passed: 1172
Thread-101My name is Thread-0
NO. 99 reader finished. Time passed: 1182
~~~
接下來我們改造下程序,改為 ReadWriteLock,代碼如下:
~~~java
public class ReadWriteLockExample {
String myName;
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public void printMyName() {
lock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "My name is " + myName);
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.readLock().unlock();
}
}
public void setMyName() {
lock.writeLock().lock();
try {
myName=Thread.currentThread().getName();
System.out.println(Thread.currentThread().getName() + "set my name to " + myName);
} finally {
lock.writeLock().unlock();
}
}
public static void main(String[] args) {
Long startTime = new Date().getTime();
ReadWriteLockExample example = new ReadWriteLockExample();
new Thread(()->{
while (true){
example.setMyName();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(()->{
while (true){
example.setMyName();
try {
Thread.sleep(new Random().nextInt(10));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
IntStream.range(0,100).forEach(num->{
new Thread(()->{
example.printMyName();
System.out.println("NO. "+num+" reader finished. Time passed: "+(new Date().getTime()-startTime));
}).start();
});
}
}
~~~
可以看到只是把鎖換成了 ReentrantReadWriteLock,然后 printMyName 中使用讀鎖,setMyName 中使用寫鎖。運行后輸出如下:
~~~
NO. 92 reader finished. Time passed: 97
NO. 96 reader finished. Time passed: 97
Thread-0set my name to Thread-0
NO. 98 reader finished. Time passed: 97
Thread-1set my name to Thread-1
NO. 99 reader finished. Time passed: 97
~~~
最后一個 reader 完成工作,只用了 97ms,對比起使用互斥鎖的 1182ms,速度提升了 10倍以上!原因就是讀操作之間不會互斥,可以并發讀取。從而性能大幅度得到提升。
## 3、ReadWriteLock 使用場景
從上面例子可以看出,如果讀操作遠遠多于寫操作,使用 ReadWriteLock 可以大幅提升性能。但如果是一個寫入密集型的程序,那么 ReadWriteLock 并不會帶來顯著性能的提升,因為即使使用 ReadWriteLock,“寫/寫”及“讀/寫”依舊是互斥的。并且由于要分開控制讀寫兩種鎖,還需要額外的開銷。
如果你的并發程序存在性能問題,可以把 ReadWriteLock 作為性能調優的手段,進行嘗試。究竟讀和寫的線程達到什么比例時,使用 ReadWriteLock 性能更好,其實并沒有定論。完全和你的程序場景有關系,所以使用 ReadWriteLock 做性能調優時,一定要基于實際的測試數據,而不是一股腦的全部使用 ReadWriteLock。
## 4、ReadWriteLock 實現
關于 ReadWriteLock 的實現,我們先看下面的類圖:

可以看到 ReentrantReadWriteLock 中持有 readerLock 和 writerLock 兩把鎖,而這兩把鎖也是 Lock 接口的實現。ReadLock 間由于是非互斥的,所以ReadLock對lock方法的實現如下:
~~~java
public void lock() {
sync.acquireShared(1);
}
~~~
而WriteLock對lock方法的實現則是如下:
~~~java
public void lock() {
sync.acquire(1);
}
~~~
可以看到 ReadLock 的 lock 方法中調用的是 acquireShared,也就是共享方式獲取鎖。兩者都是通過 sync 來實現,兩種鎖的 sync 對象都是來自 ReentrantReadWriteLock 的構造函數:
~~~java
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
~~~
而 Sync 之前我們已經講解過,它繼承自 AbstractQueuedSynchronizer,也就是 AQS。通過 AQS 提供的模版實現同步原語。而最終的實現方式則是在 AbstractQueuedSynchronizer 的子類,也就是 FairSync 和 NonfairSync 中。AQS 的原理之前已經講過。結合 AQS 的原理,再加上之前我們對 ReentrantLock 源代碼的分析,再來分析 ReentrantReadWriteLock 的源代碼,并不困難,大家可以自行繼續分析。
## 5、總結
本節我們又學習了一種比較實用的鎖。ReentrantReadWriteLock 允許并發的讀,如果你的程序以讀取為主,那么使用 ReentrantReadWriteLock 會顯著提升你的性能。但如果場景不符合,不但不會提升性能,還會因為鎖的復雜度,反而降級性能。
- 前言
- 第1章 Java并發簡介
- 01 開篇詞:多線程為什么是你必需要掌握的知識
- 02 絕對不僅僅是為了面試—我們為什么需要學習多線程
- 03 多線程開發如此簡單—Java中如何編寫多線程程序
- 04 人多力量未必大—并發可能會遇到的問題
- 第2章 Java中如何編寫多線程
- 05 看若兄弟,實如父子—Thread和Runnable詳解
- 06 線程什么時候開始真正執行?—線程的狀態詳解
- 07 深入Thread類—線程API精講
- 08 集體協作,什么最重要?溝通!—線程的等待和通知
- 09 使用多線程實現分工、解耦、緩沖—生產者、消費者實戰
- 第3章 并發的問題和原因詳解
- 10 有福同享,有難同當—原子性
- 11 眼見不實—可見性
- 12 什么?還有這種操作!—有序性
- 13 問題的根源—Java內存模型簡介
- 14 僵持不下—死鎖詳解
- 第4章 如何解決并發問題
- 15 原子性輕量級實現—深入理解Atomic與CAS
- 16 讓你眼見為實—volatile詳解
- 17 資源有限,請排隊等候—Synchronized使用、原理及缺陷
- 18 線程作用域內共享變量—深入解析ThreadLocal
- 第5章 線程池
- 19 自己動手豐衣足食—簡單線程池實現
- 20 其實不用造輪子—Executor框架詳解
- 第6章 主要并發工具類
- 21 更高級的鎖—深入解析Lock
- 22 到底哪把鎖更適合你?—synchronized與ReentrantLock對比
- 23 按需上鎖—ReadWriteLock詳解
- 24 經典并發容器,多線程面試必備—深入解析ConcurrentHashMap上
- 25 經典并發容器,多線程面試必備—深入解析ConcurrentHashMap下
- 26不讓我進門,我就在門口一直等!—BlockingQueue和ArrayBlockingQueue
- 27 倒數計時開始,三、二、一—CountDownLatch詳解
- 28 人齊了,一起行動—CyclicBarrier詳解
- 29 一手交錢,一手交貨—Exchanger詳解
- 30 限量供應,不好意思您來晚了—Semaphore詳解
- 第7章 高級并發工具類及并發設計模式
- 31 憑票取餐—Future模式詳解
- 32 請按到場順序發言—Completion Service詳解
- 33 分階段執行你的任務-學習使用Phaser運行多階段任務
- 34 誰都不能偷懶-通過 CompletableFuture 組裝你的異步計算單元
- 35 拆分你的任務—學習使用Fork/Join框架
- 36 為多線程們安排一位經理—Master/Slave模式詳解
- 第8章 總結
- 37 結束語