歡迎訪問我的GitHub
這里分類和匯總了欣宸的全部原創(chuàng)(含配套源碼):https://github.com/zq2599/blog_demos
本篇概覽
- 本篇是《quarkus依賴注入》的第九篇,目標是在輕松的氣氛中學習一個小技能:bean鎖
- quarkus的bean鎖本身很簡單:用兩個注解修飾bean和方法即可,但涉及到多線程同步問題,欣宸愿意花更多篇幅與各位Java程序員一起暢談多線程,聊個痛快,本篇由以下內容組成
- 關于多線程同步問題
- 代碼復現(xiàn)多線程同步問題
- quarkus的bean讀寫鎖
關于讀寫鎖
- java的并發(fā)包中有讀寫鎖ReadWriteLock:在多線程場景中,如果某個對象處于改變狀態(tài),可以用寫鎖加鎖,這樣所有做讀操作對象的線程,在獲取讀鎖時就會block住,直到寫鎖釋放
- 為了演示bean鎖的效果,咱們先來看一個經(jīng)典的多線程同步問題,如下圖,余額100,充值10塊,扣費5塊,正常情況下最終余額應該是105,但如果充值和扣費是在兩個線程同時進行,而且各算各的,再分別用自己的計算結果去覆蓋余額,最終會導致計算不準確

代碼復現(xiàn)多線程同步問題
- 咱們用代碼來復現(xiàn)上圖中的問題,AccountBalanceService是個賬號服務類,其成員變量accountBalance表示余額,另外有三個方法,功能分別是:
- get:返回余額,相當于查詢余額服務
- deposit:充值,入?yún)⑹浅渲到痤~,方法內將余額放入臨時變量,然后等待100毫秒模擬耗時操作,再將臨時變量與入?yún)⒌暮蛯懭氤蓡T變量accountBalance
- deduct:扣費,入?yún)⑹强圪M金額,方法內將余額放入臨時變量,然后等待100毫秒模擬耗時操作,再將臨時變量與入?yún)⒌牟顚懭氤蓡T變量accountBalance
- AccountBalanceService.java源碼如下,deposit和deduct這兩個方法各算各的,絲毫沒有考慮當時其他線程對accountBalance的影響
package com.bolingcavalry.service.impl;
import io.quarkus.logging.Log;
import javax.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class AccountBalanceService {
// 賬戶余額,假設初始值為100
int accountBalance = 100;
/**
* 查詢余額
* @return
*/
public int get() {
// 模擬耗時的操作
try {
Thread.sleep(80);
} catch (InterruptedException e) {
e.printStackTrace();
}
return accountBalance;
}
/**
* 模擬了一次充值操作,
* 將賬號余額讀取到本地變量,
* 經(jīng)過一秒鐘的計算后,將計算結果寫入賬號余額,
* 這一秒內,如果賬號余額發(fā)生了變化,就會被此方法的本地變量覆蓋,
* 因此,多線程的時候,如果其他線程修改了余額,那么這里就會覆蓋掉,導致多線程同步問題,
* AccountBalanceService類使用了Lock注解后,執(zhí)行此方法時,其他線程執(zhí)行AccountBalanceService的方法時就會block住,避免了多線程同步問題
* @param value
* @throws InterruptedException
*/
public void deposit(int value) {
// 先將accountBalance的值存入tempValue變量
int tempValue = accountBalance;
Log.infov("start deposit, balance [{0}], deposit value [{1}]", tempValue, value);
// 模擬耗時的操作
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
tempValue += value;
// 用tempValue的值覆蓋accountBalance,
// 這個tempValue的值是基于100毫秒前的accountBalance計算出來的,
// 如果這100毫秒期間其他線程修改了accountBalance,就會導致accountBalance不準確的問題
// 例如最初有100塊,這里存了10塊,所以余額變成了110,
// 但是這期間如果另一線程取了5塊,那余額應該是100-5+10=105,但是這里并沒有靠攏100-5,而是很暴力的將110寫入到accountBalance
accountBalance = tempValue;
Log.infov("end deposit, balance [{0}]", tempValue);
}
/**
* 模擬了一次扣費操作,
* 將賬號余額讀取到本地變量,
* 經(jīng)過一秒鐘的計算后,將計算結果寫入賬號余額,
* 這一秒內,如果賬號余額發(fā)生了變化,就會被此方法的本地變量覆蓋,
* 因此,多線程的時候,如果其他線程修改了余額,那么這里就會覆蓋掉,導致多線程同步問題,
* AccountBalanceService類使用了Lock注解后,執(zhí)行此方法時,其他線程執(zhí)行AccountBalanceService的方法時就會block住,避免了多線程同步問題
* @param value
* @throws InterruptedException
*/
public void deduct(int value) {
// 先將accountBalance的值存入tempValue變量
int tempValue = accountBalance;
Log.infov("start deduct, balance [{0}], deposit value [{1}]", tempValue, value);
// 模擬耗時的操作
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
tempValue -= value;
// 用tempValue的值覆蓋accountBalance,
// 這個tempValue的值是基于100毫秒前的accountBalance計算出來的,
// 如果這100毫秒期間其他線程修改了accountBalance,就會導致accountBalance不準確的問題
// 例如最初有100塊,這里存了10塊,所以余額變成了110,
// 但是這期間如果另一線程取了5塊,那余額應該是100-5+10=105,但是這里并沒有靠攏100-5,而是很暴力的將110寫入到accountBalance
accountBalance = tempValue;
Log.infov("end deduct, balance [{0}]", tempValue);
}
}
- 接下來是單元測試類LockTest.java,有幾處需要注意的地方稍后會說明
package com.bolingcavalry;
import com.bolingcavalry.service.impl.AccountBalanceService;
import io.quarkus.logging.Log;
import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import javax.inject.Inject;
import java.util.concurrent.CountDownLatch;
@QuarkusTest
public class LockTest {
@Inject
AccountBalanceService account;
@Test
public void test() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(3);
int initValue = account.get();
final int COUNT = 10;
// 這是個只負責讀取的線程,循環(huán)讀10次,每讀一次就等待50毫秒
new Thread(() -> {
for (int i=0;i<COUNT;i++) {
// 讀取賬號余額
Log.infov("current balance {0}", account.get());
}
latch.countDown();
}).start();
// 這是個充值的線程,循環(huán)充10次,每次存2元
new Thread(() -> {
for (int i=0;i<COUNT;i++) {
account.deposit(2);
}
latch.countDown();
}).start();
// 這是個扣費的線程,循環(huán)扣10次,每取1元
new Thread(() -> {
for (int i=0;i<COUNT;i++) {
account.deduct(1);
}
latch.countDown();
}).start();
latch.await();
int finalValue = account.get();
Log.infov("finally, current balance {0}", finalValue);
Assertions.assertEquals(initValue + COUNT, finalValue);
}
}
- 上述代碼中,有以下幾點需要注意
- 在主線程中新增了三個子線程,分別執(zhí)行查詢、充值、扣費的操作,可見deposit和deduct方法是并行執(zhí)行的
- 初始余額100,充值一共20元,扣費一共10元,因此最終正確結果應該是110元
- 為了確保三個子線程全部執(zhí)行完畢后主線程才退出,這里用了CountDownLatch,在執(zhí)行latch.await()的時候主線程就開始等待了,等到三個子線程把各自的latch.await()都執(zhí)行后,主線程才會繼續(xù)執(zhí)行
- 最終會檢查余額是否等于110,如果不是則單元測試不通過
- 執(zhí)行單元測試,結果如下圖,果然失敗了

- 來分析測試過程中的日志,有助于我們理解問題的原因,如下圖,充值和扣費同時開始,充值先完成,此時余額是102,但是扣費無視102,依舊使用100作為余額去扣費,然后將扣費結果99寫入余額,導致余額與正確的邏輯產(chǎn)生差距
- 反復運行上述單元測試,可以發(fā)現(xiàn)每次得到的結果都不一樣,這算是典型的多線程同步問題了吧...
- 看到這里,經(jīng)驗豐富的您應該想到了多種解決方式,例如下面這五種都可以:
- 用傳統(tǒng)的synchronized關鍵字修飾三個方法
- java包的讀寫鎖
- deposit和deduct方法內部,不要使用臨時變量tempValue,將余額的類型從int改成AtomicInteger,再使用addAndGet方法計算并設置
- 用MySQL的樂觀鎖
- 用Redis的分布式鎖
- 沒錯,上述方法都能解決問題,現(xiàn)在除了這些,quarku還從bean的維度為我們提供了一種新的方法:bean讀寫鎖,接下來細看這個bean讀寫鎖
Container-managed Concurrency:quarkus基于bean的讀寫鎖方案
- quarkus為bean提供了讀寫鎖方案:Lock注解,借助它,可以為bean的所有方法添加同一把寫鎖,再手動將讀鎖添加到指定的讀方法,這樣在多線程操作的場景下,也能保證數(shù)據(jù)的正確性
- 來看看Lock注解源碼,很簡單的幾個屬性,要重點注意的是:默認屬性為Type.WRITE,也就是寫鎖,被Lock修飾后,鎖類型有三種選擇:讀鎖,寫鎖,無鎖
@InterceptorBinding
@Inherited
@Target(value = { TYPE, METHOD })
@Retention(value = RUNTIME)
public @interface Lock {
/**
*
* @return the type of the lock
*/
@Nonbinding
Type value() default Type.WRITE;
/**
* If it's not possible to acquire the lock in the given time a {@link LockException} is thrown.
*
* @see java.util.concurrent.locks.Lock#tryLock(long, TimeUnit)
* @return the wait time
*/
@Nonbinding
long time() default -1l;
/**
*
* @return the wait time unit
*/
@Nonbinding
TimeUnit unit() default TimeUnit.MILLISECONDS;
public enum Type {
/**
* Acquires the read lock before the business method is invoked.
*/
READ,
/**
* Acquires the write (exclusive) lock before the business method is invoked.
*/
WRITE,
/**
* Acquires no lock.
* <p>
* This could be useful if you need to override the behavior defined by a class-level interceptor binding.
*/
NONE
}
}
-
接下來看看如何用bean鎖解AccountBalanceService的多線程同步問題
-
為bean設置讀寫鎖很簡單,如下圖紅框1,給類添加Lock注解后,AccountBalanceService的每個方法都默認添加了寫鎖,如果想修改某個方法的鎖類型,可以像紅框2那樣指定,Lock.Type.READ表示將get方法改為讀鎖,如果不想給方法上任何鎖,就使用Lock.Type.NONE

- 這里預測一下修改后的效果
- 在deposit和deduct都沒有被調用時,get方法可以被調用,而且可以多線程同時調用,因為每個線程都能順利拿到讀鎖
- 一旦deposit或者deduct被調用,其他線程在調用deposit、deduct、get方法時都被阻塞了,因為此刻不論讀鎖還是寫鎖都拿不到,必須等deposit執(zhí)行完畢,它們才重新去搶鎖
- 有了上述邏輯,再也不會出現(xiàn)deposit和deduct同時修改余額的情況了,預測單元測試應該能通過
- 這種讀寫鎖的方法雖然可以確保邏輯正確,但是代價不?。ㄒ粋€線程執(zhí)行,其他線程等待),所以在并發(fā)性能要求較高的場景下要慎用,可以考慮樂觀鎖、AtomicInteger這些方式來降低等待代價
- 再次運行單元測試,如下圖,測試通過

- 再來看看測試過程中的日志,如下圖,之前的幾個方法同時執(zhí)行的情況已經(jīng)消失了,每個方法在執(zhí)行的時候,其他線程都在等待
文章來源:http://www.zghlxwxcb.cn/news/detail-632054.html
- 至此,bean鎖知識點學習完畢,希望本篇能給您一些參考,為您的并發(fā)編程中添加新的方案
源碼下載
- 本篇實戰(zhàn)的完整源碼可在GitHub下載到,地址和鏈接信息如下表所示(https://github.com/zq2599/blog_demos)
名稱 | 鏈接 | 備注 |
---|---|---|
項目主頁 | https://github.com/zq2599/blog_demos | 該項目在GitHub上的主頁 |
git倉庫地址(https) | https://github.com/zq2599/blog_demos.git | 該項目源碼的倉庫地址,https協(xié)議 |
git倉庫地址(ssh) | git@github.com:zq2599/blog_demos.git | 該項目源碼的倉庫地址,ssh協(xié)議 |
- 這個git項目中有多個文件夾,本次實戰(zhàn)的源碼在quarkus-tutorials文件夾下,如下圖紅框
-
quarkus-tutorials是個父工程,里面有多個module,本篇實戰(zhàn)的module是basic-di,如下圖紅框
歡迎關注博客園:程序員欣宸
學習路上,你不孤單,欣宸原創(chuàng)一路相伴...文章來源地址http://www.zghlxwxcb.cn/news/detail-632054.html
到了這里,關于quarkus依賴注入之九:bean讀寫鎖的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!