StampedLock,一種比讀寫鎖更快的鎖!
01、背景介紹
在上一篇文章中,我們講到了使用ReadWriteLock可以解決多線程同時(shí)讀,但只有一個(gè)線程能寫的問題。
如果繼續(xù)深入的分析ReadWriteLock,從鎖的角度分析,會(huì)發(fā)現(xiàn)它有一個(gè)潛在的問題:如果有線程正在讀數(shù)據(jù),寫線程準(zhǔn)備修改數(shù)據(jù)的時(shí)候,需要等待讀線程釋放鎖后才能獲取寫鎖,簡單的說就是,讀的過程中不允許寫,這其實(shí)是一種悲觀的讀鎖。
為了進(jìn)一步的提升程序并發(fā)執(zhí)行效率,Java 8 引入了一個(gè)新的讀寫鎖:StampedLock。
與ReadWriteLock相比,StampedLock最大的改進(jìn)點(diǎn)在于:在原先讀寫鎖的基礎(chǔ)上,新增了一種叫樂觀讀的模式。該模式并不會(huì)加鎖,因此不會(huì)阻塞線程,程序會(huì)有更高的執(zhí)行效率。
什么是樂觀鎖和悲觀鎖呢?
- 樂觀鎖:就是樂觀的估計(jì)讀的過程中大概率不會(huì)有寫入,因此被稱為樂觀鎖
- 悲觀鎖:指的是讀的過程中拒絕有寫入,也就是寫入必須等待
顯然樂觀鎖的并發(fā)執(zhí)行效率會(huì)更高,但一旦有數(shù)據(jù)的寫入導(dǎo)致讀取的數(shù)據(jù)不一致,需要能檢測出來,再讀一遍就行。
下面我們一起來了解一下StampedLock的用法!
02、StampedLock 用法介紹
StampedLock的使用方式比較簡單,只需要實(shí)例化一個(gè)StampedLock對(duì)象,然后調(diào)用對(duì)應(yīng)的讀寫方法即可,它有三個(gè)核心方法如下!
- readLock():表示讀鎖,多個(gè)線程讀不會(huì)阻塞,效果與ReadWriteLock的讀鎖模式類似
- writeLock():表示寫鎖,同一時(shí)刻有且只有一個(gè)寫線程能獲取鎖資源,效果與ReadWriteLock的寫鎖模式類似
- tryOptimisticRead():表示樂觀讀,并沒有加鎖,它用于非常短的讀操作,允許多個(gè)線程同時(shí)讀
其中readLock()和writeLock()方法,與ReadWriteLock的效果完全一致,在此就不重復(fù)演示了。
下面我們來看一個(gè)tryOptimisticRead()方法的簡單使用示例。
2.1、tryOptimisticRead 方法
public class CounterDemo {
private final StampedLock lock = new StampedLock();
private int count;
public void write() {
// 1.獲取寫鎖
long stamp = lock.writeLock();
try {
count++;
// 方便演示,休眠一下
sleep(200);
println("獲得了寫鎖,count:" + count);
} finally {
// 2.釋放寫鎖
lock.unlockWrite(stamp);
}
}
public int read() {
// 1.嘗試通過樂觀讀模式讀取數(shù)據(jù),非阻塞
long stamp = lock.tryOptimisticRead();
// 2.假設(shè)x = 0,但是x可能被寫線程修改為1
int x = count;
// 方便演示,休眠一下
int millis = new Random().nextInt(500);
sleep(millis);
println("通過樂觀讀模式讀取數(shù)據(jù),value:" + x + ", 耗時(shí):" + millis);
// 3.檢查樂觀讀后是否有其他寫鎖發(fā)生
if(!lock.validate(stamp)){
// 4.如果有,采用悲觀讀鎖,并重新讀取數(shù)據(jù)到當(dāng)前線程局部變量
stamp = lock.readLock();
try {
x = count;
println("樂觀讀后檢查到數(shù)據(jù)發(fā)生變化,獲得了讀鎖,value:" + x);
} finally{
// 5.釋放悲觀讀鎖
lock.unlockRead(stamp);
}
}
// 6.返回讀取的數(shù)據(jù)
return x;
}
private void sleep(long millis){
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void println(String message){
String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS").format(new Date());
System.out.println(time + " 線程:" + Thread.currentThread().getName() + " " + message);
}
}
public class MyThreadTest {
public static void main(String[] args) throws InterruptedException {
CounterDemo counter = new CounterDemo();
Runnable readRunnable = new Runnable() {
@Override
public void run() {
counter.read();
}
};
Runnable writeRunnable = new Runnable() {
@Override
public void run() {
counter.write();
}
};
// 啟動(dòng)3個(gè)讀線程
for (int i = 0; i < 3; i++) {
new Thread(readRunnable).start();
}
// 停頓一下
Thread.sleep(300);
// 啟動(dòng)3個(gè)寫線程
for (int i = 0; i < 3; i++) {
new Thread(writeRunnable).start();
}
}
}
看一下運(yùn)行結(jié)果:
2023-10-25 13:47:16:952 線程:Thread-0 通過樂觀讀模式讀取數(shù)據(jù),value:0, 耗時(shí):19
2023-10-25 13:47:17:050 線程:Thread-2 通過樂觀讀模式讀取數(shù)據(jù),value:0, 耗時(shí):172
2023-10-25 13:47:17:247 線程:Thread-1 通過樂觀讀模式讀取數(shù)據(jù),value:0, 耗時(shí):369
2023-10-25 13:47:17:382 線程:Thread-3 獲得了寫鎖,count:1
2023-10-25 13:47:17:586 線程:Thread-4 獲得了寫鎖,count:2
2023-10-25 13:47:17:788 線程:Thread-5 獲得了寫鎖,count:3
2023-10-25 13:47:17:788 線程:Thread-1 樂觀讀后檢查到數(shù)據(jù)發(fā)生變化,獲得了讀鎖,value:3
從日志上可以分析得出,讀線程Thread-0和Thread-2在啟動(dòng)寫線程之前就已經(jīng)執(zhí)行完,因此沒有進(jìn)入競爭讀鎖階段;而讀線程Thread-1因?yàn)樵趩?dòng)寫線程之后才執(zhí)行完,這個(gè)時(shí)候檢查到數(shù)據(jù)發(fā)生變化,因此進(jìn)入讀鎖階段,保證讀取的數(shù)據(jù)是最新的。
和ReadWriteLock相比,StampedLock寫入數(shù)據(jù)的加鎖過程基本類似,不同的是讀取數(shù)據(jù)。
讀取數(shù)據(jù)大致的過程如下:
1.嘗試通過tryOptimisticRead()方法樂觀讀模式讀取數(shù)據(jù),并返回版本號(hào)
2.數(shù)據(jù)讀取完成后,再通過lock.validate()去驗(yàn)證版本號(hào),如果在讀取過程中沒有寫入,版本號(hào)不會(huì)變,驗(yàn)證成功,直接返回結(jié)果
3.如果在讀取過程中有寫入,版本號(hào)會(huì)發(fā)生變化,驗(yàn)證將失敗。在失敗的時(shí)候,再通過悲觀讀鎖再次讀取數(shù)據(jù),把讀取的最新結(jié)果返回
對(duì)于讀多寫少的場景,由于寫入的概率不高,程序在絕大部分情況下可以通過樂觀讀獲取數(shù)據(jù),極少數(shù)情況下使用悲觀讀鎖獲取數(shù)據(jù),并發(fā)執(zhí)行效率得到了大大的提升。
樂觀鎖實(shí)際用途也非常廣泛,比如數(shù)據(jù)庫的字段值修改,我們舉個(gè)簡單的例子。
在訂單庫存表上order_store,我們通常會(huì)增加了一個(gè)數(shù)值型版本號(hào)字段version,每次更新order_store這個(gè)表庫存數(shù)據(jù)的時(shí)候,都將version字段加1,同時(shí)檢查version的值是否滿足條件。
select id,... ,version
from order_store
where id = 1000
update order_store
set version = version + 1,...
where id = 1000 and version = 1
數(shù)據(jù)庫的樂觀鎖,就是查詢的時(shí)候?qū)ersion查出來,更新的時(shí)候利用version字段驗(yàn)證是否一致,如果相等,說明數(shù)據(jù)沒有被修改,讀取的數(shù)據(jù)安全;如果不相等,說明數(shù)據(jù)已經(jīng)被修改過,讀取的數(shù)據(jù)不安全,需要重新讀取。
這里的version就類似于StampedLock的stamp值。
2.2、tryConvertToWriteLock 方法
其次,StampedLock還提供了將悲觀讀鎖升級(jí)為寫鎖的功能,對(duì)應(yīng)的核心方法是tryConvertToWriteLock()。
它主要使用在if-then-update的場景,即:程序先采用讀模式,如果讀的數(shù)據(jù)滿足條件,就返回;如果讀的數(shù)據(jù)不滿足條件,再嘗試寫。
簡單示例如下:
public int readAndWrite(Integer newCount) {
// 1.獲取讀鎖,也可以使用樂觀讀
long stamp = lock.readLock();
int currentValue = count;
try {
// 2.檢查是否讀取數(shù)據(jù)
while (Objects.isNull(currentValue)) {
// 3.如果沒有,嘗試升級(jí)寫鎖
long wl = lock.tryConvertToWriteLock(stamp);
// 4.不為 0 升級(jí)寫鎖成功
if (wl != 0L) {
// 重新賦值
stamp = wl;
count = newCount;
currentValue = count;
break;
} else {
// 5.升級(jí)失敗,釋放之前加的讀鎖并上寫鎖,通過循環(huán)再試
lock.unlockRead(stamp);
stamp = lock.writeLock();
}
}
} finally {
// 6.釋放最后加的鎖
lock.unlock(stamp);
}
// 7.返回讀取的數(shù)據(jù)
return currentValue;
}
03、小結(jié)
總結(jié)下來,與ReadWriteLock相比,StampedLock進(jìn)一步把讀鎖細(xì)分為樂觀讀和悲觀讀,能進(jìn)一步提升了并發(fā)執(zhí)行效率。
好處是非常明顯的,系統(tǒng)性能得到提升,但是代價(jià)也不小,主要有以下幾點(diǎn):
- 1.代碼邏輯更加復(fù)雜,如果編程不當(dāng)很容易出 bug
- 2.StampedLock是不可重入鎖,不能在一個(gè)線程中反復(fù)獲取同一個(gè)鎖,如果編程不當(dāng),很容易出現(xiàn)死鎖
- 3.如果線程阻塞在StampedLock的readLock()或者writeLock()方法上時(shí),此時(shí)試圖通過interrupt()方法中斷線程,會(huì)導(dǎo)致 CPU 飆升。因此,使用 StampedLock一定不要調(diào)用中斷操作,如果需要支持中斷功能,推薦使用可中斷的讀鎖readLockInterruptibly()或者寫鎖writeLockInterruptibly()方法。
最后,在實(shí)際的使用過程中,樂觀讀編程模型,推薦可以按照以下固定模板編寫。
public int read() {
// 1.嘗試通過樂觀讀模式讀取數(shù)據(jù),非阻塞
long stamp = lock.tryOptimisticRead();
// 2.假設(shè)x = 0,但是x可能被寫線程修改為1
int x = count;
// 3.檢查樂觀讀后是否有其他寫鎖發(fā)生
if(!lock.validate(stamp)){
// 4.如果有,采用悲觀讀鎖,并重新讀取數(shù)據(jù)到當(dāng)前線程局部變量
stamp = lock.readLock();
try {
x = count;
} finally{
// 5.釋放悲觀讀鎖
lock.unlockRead(stamp);
}
}
// 6.返回讀取的數(shù)據(jù)
return x;
}
04、參考
1、https://www.liaoxuefeng.com
2、https://zhuanlan.zhihu.com/p/257868603