欢迎访问我的GitHub
这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos
本篇概览
- 本篇是《quarkus依赖注入》的第九篇,目标是在轻松的气氛中学习一个小技能:bean锁
- quarkus的bean锁本身很简单:用两个注解修饰bean和方法即可,但涉及到多线程同步问题,欣宸愿意花更多篇幅与各位Java程序员一起畅谈多线程,聊个痛快,本篇由以下内容组成
- 关于多线程同步问题
- 代码复现多线程同步问题
- quarkus的bean读写锁
关于读写锁
- java的并发包中有读写锁ReadWriteLock:在多线程场景中,如果某个对象处于改变状态,可以用写锁加锁,这样所有做读操作对象的线程,在获取读锁时就会block住,直到写锁释放
- 为了演示bean锁的效果,咱们先来看一个经典的多线程同步问题,如下图,余额100,充值10块,扣费5块,正常情况下最终余额应该是105,但如果充值和扣费是在两个线程同时进行,而且各算各的,再分别用自己的计算结果去覆盖余额,最终会导致计算不准确
代码复现多线程同步问题
- 咱们用代码来复现上图中的问题,AccountBalanceService是个账号服务类,其成员变量accountBalance表示余额,另外有三个方法,功能分别是:
- get:返回余额,相当于查询余额服务
- deposit:充值,入参是充值金额,方法内将余额放入临时变量,然后等待100毫秒模拟耗时操作,再将临时变量与入参的和写入成员变量accountBalance
- deduct:扣费,入参是扣费金额,方法内将余额放入临时变量,然后等待100毫秒模拟耗时操作,再将临时变量与入参的差写入成员变量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;
- }
- /**
- * 模拟了一次充值操作,
- * 将账号余额读取到本地变量,
- * 经过一秒钟的计算后,将计算结果写入账号余额,
- * 这一秒内,如果账号余额发生了变化,就会被此方法的本地变量覆盖,
- * 因此,多线程的时候,如果其他线程修改了余额,那么这里就会覆盖掉,导致多线程同步问题,
- * AccountBalanceService类使用了Lock注解后,执行此方法时,其他线程执行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);
- }
- /**
- * 模拟了一次扣费操作,
- * 将账号余额读取到本地变量,
- * 经过一秒钟的计算后,将计算结果写入账号余额,
- * 这一秒内,如果账号余额发生了变化,就会被此方法的本地变量覆盖,
- * 因此,多线程的时候,如果其他线程修改了余额,那么这里就会覆盖掉,导致多线程同步问题,
- * AccountBalanceService类使用了Lock注解后,执行此方法时,其他线程执行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,有几处需要注意的地方稍后会说明
[code]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;@QuarkusTestpublic class LockTest { @Inject AccountBalanceService account; @Test public void test() throws InterruptedException { CountDownLatch latch = new CountDownLatch(3); int initValue = account.get(); final int COUNT = 10; // 这是个只负责读取的线程,循环读10次,每读一次就等待50毫秒 new Thread(() -> { for (int i=0;i { for (int i=0;i { for (int i=0;i |