應用情景
前一陣有個做反抄襲檢測的小夥伴問了我一個問題。
---
在多執行緒裡就是有個變數,我需要讀取它來判斷是否給它寫入一些資訊。
打算加鎖,但是如果讀取時候加入readlock,寫入時候加入writelock,
這樣做可能讀寫不同步。但是如果一起加lock效果就跟synchronized一樣,效率變差
---複製程式碼
其實他的問題就是下面的場景:
- 高併發的讀寫請求
- 讀取請求明顯大於寫入的請求
- 如果用synchronized同步鎖會導致效能下降,本來讀取是可以多執行緒同步進行的,同步鎖就只能讓他們一個一個排隊讀取。
- 如果讀取時加readlock,寫入時候加writelock,會提升效率,因為讀可以多執行緒併發,但是線上程A讀完上鎖的毫秒級時間裡,有可能執行緒B也讀完了,而且搶在了執行緒A之前修改了變數,導致程式出錯。
處理方案
我去研究了下讀寫鎖,後來發現其實java官方已經給了答案。使用讀寫鎖加標誌位解決讀寫不同步的問題。Java ReentrantReadWriteLock
在這先普及下讀寫鎖。
- 讀鎖:
- 讀鎖拒絕其他執行緒獲得寫鎖,讀鎖不拒絕其他執行緒獲得讀鎖,多個上了讀鎖的執行緒可以併發讀不會阻塞。
- 多個讀鎖同時作用期間,其他想上寫鎖的執行緒都處在等待狀態,當最後一個讀鎖釋放後,才有可能上鎖。
- 寫鎖:
- 寫鎖拒絕其他執行緒獲取讀鎖和寫鎖。
- 當一個執行緒獲取寫鎖後,其他想要獲取讀寫鎖的執行緒都處於等待狀態,直到寫鎖釋放才有可能上鎖。
##程式碼例項
直接拿Java官方示例解讀了。
class CachedData {
Object data;
volatile boolean cacheValid;
final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCachedData() {
rwl.readLock().lock(); //1. 上讀鎖
if (!cacheValid) { //2. 驗證cacheValid
// Must release read lock before acquiring write lock
rwl.readLock().unlock(); //3. 解除讀鎖
rwl.writeLock().lock(); //4. 上寫鎖
try {
// Recheck state because another thread might have
// acquired write lock and changed state before we did.
if (!cacheValid) { //5. 驗證cacheValid
data = ...
cacheValid = true;
}
// Downgrade by acquiring read lock before releasing write lock
rwl.readLock().lock(); //6. 上讀鎖
} finally {
rwl.writeLock().unlock(); // Unlock write, still hold read //7. 解除寫鎖
}
}
try {
use(data);
} finally {
rwl.readLock().unlock();//8. 解除讀鎖
}
}
}複製程式碼
比如現在有執行緒ABCDE五個執行緒,使用processCachedData()方法,接下來會發現如下步驟。
-> DE執行緒滯後,ABC同時進入到步驟1. 上讀鎖
-> ABC進入到步驟2. 驗證cacheValid。(新例項中cacheValid初始為fasle,所以進入if條件句中)
-> ABC執行步驟3. 解除讀鎖。
-> 假設此時A率先完成步驟4. 上寫鎖。
-> BC無法獲取寫鎖,處於等待狀態,被阻塞在步驟4。 上寫鎖。此時D執行緒執行使用processCachedData()方法,被阻塞在步驟1. 上讀鎖。
-> A進入到步驟5. 驗證cacheValid。
步驟五很關鍵,如果執行緒A寫完後,解除了寫鎖,此時新的執行緒E獲取到了寫鎖,就會寫入新資料,此時就不是同步鎖了,程式出錯。
-> A修改資料,cacheValid置為true。步驟6和步驟7是寫鎖的降級操作,即寫鎖釋放的時候,先降級為讀鎖,這樣其他等待獲取寫鎖的執行緒會繼續等待,然後再釋放寫鎖,保證同步性。
-> A執行完步驟8,BC阻塞結束,其中一位獲取寫鎖。