欢送拜访我的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;@ApplicationScopedpublic 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,有几处须要留神的中央稍后会阐明
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<COUNT;i++) { // 读取账号余额 Log.infov("current balance {0}", account.get()); } latch.countDown(); }).start(); // 这是个充值的线程,循环充10次,每次存2元 new Thread(() -> { for (int i=0;i<COUNT;i++) { account.deposit(2); } latch.countDown(); }).start(); // 这是个扣费的线程,循环扣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); }}
- 在主线程中新增了三个子线程,别离执行查问、充值、扣费的操作,可见deposit和deduct办法是并行执行的
- 初始余额100,充值一共20元,扣费一共10元,因而最终正确后果应该是110元
- 为了确保三个子线程全副执行结束后主线程才退出,这里用了CountDownLatch,在执行latch.await()的时候主线程就开始期待了,等到三个子线程把各自的latch.await()都执行后,主线程才会继续执行
- 最终会查看余额是否等于110,如果不是则单元测试不通过
- 来分析测试过程中的日志,有助于咱们了解问题的起因,如下图,充值和扣费同时开始,充值先实现,此时余额是102,然而扣费忽视102,仍旧应用100作为余额去扣费,而后将扣费后果99写入余额,导致余额与正确的逻辑产生差距
- 重复运行上述单元测试,能够发现每次失去的后果都不一样,这算是典型的多线程同步问题了吧...
- 看到这里,经验丰富的您应该想到了多种解决形式,例如上面这五种都能够:
- 用传统的synchronized关键字润饰三个办法
- java包的读写锁
- deposit和deduct办法外部,不要应用长期变量tempValue,将余额的类型从int改成AtomicInteger,再应用addAndGet办法计算并设置
- 用MySQL的乐观锁
- 用Redis的分布式锁
- 没错,上述办法都能解决问题,当初除了这些,quarku还从bean的维度为咱们提供了一种新的办法:bean读写锁,接下来细看这个bean读写锁
Container-managed Concurrency:quarkus基于bean的读写锁计划
- quarkus为bean提供了读写锁计划:Lock注解,借助它,能够为bean的所有办法增加同一把写锁,再手动将读锁增加到指定的读办法,这样在多线程操作的场景下,也能保证数据的正确性
- 来看看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执行结束,它们才从新去抢锁
- 有了上述逻辑,再也不会呈现deposit和deduct同时批改余额的状况了,预测单元测试应该能通过
- 这种读写锁的办法尽管能够确保逻辑正确,然而代价不小(一个线程执行,其余线程期待),所以在并发性能要求较高的场景下要慎用,能够思考乐观锁、AtomicInteger这些形式来升高期待代价
- 再来看看测试过程中的日志,如下图,之前的几个办法同时执行的状况曾经隐没了,每个办法在执行的时候,其余线程都在期待
- 至此,bean锁知识点学习结束,心愿本篇能给您一些参考,为您的并发编程中增加新的计划
源码下载
- 本篇实战的残缺源码可在GitHub下载到,地址和链接信息如下表所示(https://github.com/zq2599/blog_demos)
名称 | 链接 | 备注 |
---|
我的项目主页 | https://github.com/zq2599/blog_demos | 该我的项目在GitHub上的主页 |
git仓库地址(https) | https://github.com/zq2599/blog_demos.git | 该我的项目源码的仓库地址,https协定 |
git仓库地址(ssh) | git@github.com:zq2599/blog_demos.git | 该我的项目源码的仓库地址,ssh协定 |
- 这个git我的项目中有多个文件夹,本次实战的源码在quarkus-tutorials文件夹下,如下图红框
- quarkus-tutorials是个父工程,外面有多个module,本篇实战的module是basic-di,如下图红框
欢送关注思否:程序员欣宸
学习路上,你不孤独,欣宸原创一路相伴...