前面介紹的ReadWriteLock可以解決多線程同時讀,但只有一個線程能寫的問題。
如果我們深入分析ReadWriteLock,會發現它有個潛在的問題:如果有線程正在讀,寫線程需要等待讀線程釋放鎖後才能獲取寫鎖,即讀的過程中不允許寫,這是一種悲觀的讀鎖。
要進一步提升併發執行效率,Java 8引入了新的讀寫鎖:StampedLock。
StampedLock和ReadWriteLock相比,改進之處在於:讀的過程中也允許獲取寫鎖後寫入!這樣一來,我們讀的數據就可能不一致,所以,需要一點額外的代碼來判斷讀的過程中是否有寫入,這種讀鎖是一種樂觀鎖。
StampedLock三種模式
悲觀鎖:悲觀鎖則是讀的過程中拒絕有寫入,也就是寫入必須等待。
寫鎖:與讀寫鎖的寫鎖類似,寫鎖和悲觀讀是互斥的。
樂觀鎖:樂觀地估計讀的過程中大概率不會有寫入,因此被稱爲樂觀鎖。
我們來看一個 悲觀鎖 + 寫鎖的 示例:
public class StampedLockTest {
private static StampedLock lock = new StampedLock();
//緩存中存儲的數據
private static Map<String,String> cacheMap = new HashMap<String, String>();
//模擬數據庫存儲的數據
private static Map<String,String> dbMap = new HashMap<String, String>();
static {
dbMap.put("zhangsan","張三");
dbMap.put("sili","李四");
}
private static String getInfo(String name){
//獲取悲觀讀
long stamp = lock.readLock();
System.out.println(Thread.currentThread().getName()+" 獲取了悲觀讀鎖" +" 用戶名:"+name);
try {
if("zhangsan".equals(name)){
System.out.println(Thread.currentThread().getName()+" 休眠中" +" 用戶名:"+name);
Thread.sleep(3000);
System.out.println(Thread.currentThread().getName()+" 休眠結束" +" 用戶名:"+name);
}
String info = cacheMap.get(name);
if (null != info) {
return info;
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//釋放悲觀讀
lock.unlockRead(stamp);
System.out.println(Thread.currentThread().getName()+" 釋放了悲觀讀鎖" +" 用戶名:"+name);
}
//獲取寫鎖
stamp = lock.writeLock();
System.out.println(Thread.currentThread().getName()+" 獲取了寫鎖" +" 用戶名:"+name);
try {
//判斷一下緩存中是否被插入了數據
String info = cacheMap.get(name);
if (null != info) {
return info;
}
//這裏是往數據庫獲取數據
String infoByDb = dbMap.get(name);
//講數據插入緩存
cacheMap.put(name,infoByDb);
}finally {
//System.out.println("線程名:"+Thread.currentThread().getName()+" 釋放了寫鎖" +" 用戶名:"+name);
//釋放寫鎖
lock.unlockWrite(stamp);
}
return null;
}
public static void main(String[] args) {
Thread t1 = new Thread(() ->{
getInfo("zhangsan");
});
Thread t2 = new Thread(() ->{
getInfo("lisi");
});
//線程啓動
t1.start();
t2.start();
//線程同步
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
這個示例,我們打印後可以看到結果是:悲觀讀鎖與寫鎖是互斥的,那這樣的效率不是和讀寫鎖一樣嗎?爲什麼說它比讀寫鎖更快呢?
別急,我們來看它的樂觀鎖。
public class Point {
private final StampedLock stampedLock = new StampedLock();
private double x;
private double y;
public void move(double deltaX, double deltaY){
//獲取寫鎖
long stamp = stampedLock.writeLock();
try {
x += deltaX;
y += deltaY;
}finally {
//釋放寫鎖
stampedLock.unlockWrite(stamp);
}
}
public double distanceFromOrigin() {
// 獲得一個樂觀讀鎖
long stamp = stampedLock.tryOptimisticRead();
double currentX = x;
double currentY = y;
try {
//休眠3秒,目的是爲了讓其他線程修改掉成員變量的值。
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 檢查樂觀讀鎖後是否有其他寫鎖發生
if (!stampedLock.validate(stamp)) {
System.out.println("樂觀讀鎖後有其他寫鎖發生");
//若存在寫鎖發生,則獲取一個悲觀讀鎖
stamp = stampedLock.readLock();
try {
currentX = x;
currentY = y;
}finally {
// 釋放悲觀讀鎖
stampedLock.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
public static void main(String[] args) {
Point point = new Point();
Thread t1 = new Thread(() ->{
double result = point.distanceFromOrigin();
System.out.println("result==="+result);
});
t1.start();
try {
//休眠1秒,目的爲了讓線程t1能執行到獲取成員變量之後
Thread.sleep(1000);
//point.move(20,30);
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("執行完畢");
}
}
和ReadWriteLock相比,寫入的加鎖是完全一樣的,不同的是讀取。注意到首先我們通過tryOptimisticRead()獲取一個樂觀讀鎖,並返回版本號。接着進行讀取,讀取完成後,我們通過validate()去驗證版本號,如果在讀取過程中沒有寫入,版本號不變,驗證成功,我們就可以放心地繼續後續操作。如果在讀取過程中有寫入,版本號會發生變化,驗證將失敗。在失敗的時候,我們再通過獲取悲觀讀鎖再次讀取。由於寫入的概率不高,程序在絕大部分情況下可以通過樂觀讀鎖獲取數據,極少數情況下使用悲觀讀鎖獲取數據。
可見,StampedLock把讀鎖細分爲樂觀讀和悲觀讀,能進一步提升併發效率。但這也是有代價的:一是代碼更加複雜,二是StampedLock是不可重入鎖,不能在一個線程中反覆獲取同一個鎖。
使用StampedLock的注意事項
1.StampedLock屬於ReadWriteLock的子類,ReentrantReadWriteLock也是屬於ReadWriteLock的子類。StampedLock是不可重入鎖。
2.它適用於讀多寫少的情況,如果不是這中情況,請慎用,性能可能還不如synchronized。
3.StampedLock的悲觀讀鎖、寫鎖不支持條件變量。
4.千萬不能中斷阻塞的悲觀讀鎖或寫鎖,如果調用阻塞線程的interrupt(),會導致cpu飆升,如果希望StampedLock支持中斷操作,請使用readLockInterruptibly(悲觀讀鎖)與writeLockInterruptibly(寫鎖)。