quarkus依赖注入之九:bean读写锁

这篇具有很好参考价值的文章主要介绍了quarkus依赖注入之九:bean读写锁。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

欢迎访问我的GitHub

这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos

本篇概览

  • 本篇是《quarkus依赖注入》的第九篇,目标是在轻松的气氛中学习一个小技能:bean锁
  • quarkus的bean锁本身很简单:用两个注解修饰bean和方法即可,但涉及到多线程同步问题,欣宸愿意花更多篇幅与各位Java程序员一起畅谈多线程,聊个痛快,本篇由以下内容组成
  1. 关于多线程同步问题
  2. 代码复现多线程同步问题
  3. quarkus的bean读写锁

关于读写锁

  • java的并发包中有读写锁ReadWriteLock:在多线程场景中,如果某个对象处于改变状态,可以用写锁加锁,这样所有做读操作对象的线程,在获取读锁时就会block住,直到写锁释放
  • 为了演示bean锁的效果,咱们先来看一个经典的多线程同步问题,如下图,余额100,充值10块,扣费5块,正常情况下最终余额应该是105,但如果充值和扣费是在两个线程同时进行,而且各算各的,再分别用自己的计算结果去覆盖余额,最终会导致计算不准确
quarkus依赖注入之九:bean读写锁

代码复现多线程同步问题

  • 咱们用代码来复现上图中的问题,AccountBalanceService是个账号服务类,其成员变量accountBalance表示余额,另外有三个方法,功能分别是:
  1. get:返回余额,相当于查询余额服务
  2. deposit:充值,入参是充值金额,方法内将余额放入临时变量,然后等待100毫秒模拟耗时操作,再将临时变量与入参的和写入成员变量accountBalance
  3. 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,有几处需要注意的地方稍后会说明
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;

        // 这是个只负责读取的线程,循环读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);
    }
}
  • 上述代码中,有以下几点需要注意
  1. 在主线程中新增了三个子线程,分别执行查询、充值、扣费的操作,可见deposit和deduct方法是并行执行的
  2. 初始余额100,充值一共20元,扣费一共10元,因此最终正确结果应该是110元
  3. 为了确保三个子线程全部执行完毕后主线程才退出,这里用了CountDownLatch,在执行latch.await()的时候主线程就开始等待了,等到三个子线程把各自的latch.await()都执行后,主线程才会继续执行
  4. 最终会检查余额是否等于110,如果不是则单元测试不通过
  • 执行单元测试,结果如下图,果然失败了
quarkus依赖注入之九:bean读写锁
  • 来分析测试过程中的日志,有助于我们理解问题的原因,如下图,充值和扣费同时开始,充值先完成,此时余额是102,但是扣费无视102,依旧使用100作为余额去扣费,然后将扣费结果99写入余额,导致余额与正确的逻辑产生差距

quarkus依赖注入之九:bean读写锁

  • 反复运行上述单元测试,可以发现每次得到的结果都不一样,这算是典型的多线程同步问题了吧...
  • 看到这里,经验丰富的您应该想到了多种解决方式,例如下面这五种都可以:
  1. 用传统的synchronized关键字修饰三个方法
  2. java包的读写锁
  3. deposit和deduct方法内部,不要使用临时变量tempValue,将余额的类型从int改成AtomicInteger,再使用addAndGet方法计算并设置
  4. 用MySQL的乐观锁
  5. 用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

quarkus依赖注入之九:bean读写锁
  • 这里预测一下修改后的效果
  1. 在deposit和deduct都没有被调用时,get方法可以被调用,而且可以多线程同时调用,因为每个线程都能顺利拿到读锁
  2. 一旦deposit或者deduct被调用,其他线程在调用deposit、deduct、get方法时都被阻塞了,因为此刻不论读锁还是写锁都拿不到,必须等deposit执行完毕,它们才重新去抢锁
  3. 有了上述逻辑,再也不会出现deposit和deduct同时修改余额的情况了,预测单元测试应该能通过
  4. 这种读写锁的方法虽然可以确保逻辑正确,但是代价不小(一个线程执行,其他线程等待),所以在并发性能要求较高的场景下要慎用,可以考虑乐观锁、AtomicInteger这些方式来降低等待代价
  • 再次运行单元测试,如下图,测试通过
quarkus依赖注入之九:bean读写锁
  • 再来看看测试过程中的日志,如下图,之前的几个方法同时执行的情况已经消失了,每个方法在执行的时候,其他线程都在等待

quarkus依赖注入之九:bean读写锁

  • 至此,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依赖注入之九:bean读写锁
  • quarkus-tutorials是个父工程,里面有多个module,本篇实战的module是basic-di,如下图红框
    quarkus依赖注入之九:bean读写锁

欢迎关注博客园:程序员欣宸

学习路上,你不孤单,欣宸原创一路相伴...文章来源地址https://www.toymoban.com/news/detail-632054.html

到了这里,关于quarkus依赖注入之九:bean读写锁的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • quarkus依赖注入之十:学习和改变bean懒加载规则

    这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos 本篇是《quarkus依赖注入》系列的第十篇,来看一个容易被忽略的知识点:bean的懒加载,咱们先去了解quarkus框架下的懒加载规则,然后更重要的是掌握如何改变规则,以达到提前实例化的目标 总的来

    2024年02月14日
    浏览(41)
  • quarkus依赖注入之七:生命周期回调

    这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos 本篇的知识点是bean的生命周期回调:在bean生命周期的不同阶段,都可以触发自定义代码的执行 触发自定义代码执行的具体方式,是用对应的注解去修饰要执行的方法,如下图所示: 有两种模式可以

    2024年02月14日
    浏览(39)
  • quarkus依赖注入之八:装饰器(Decorator)

    这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos 本篇是《quarkus依赖注入》系列的第八篇,目标是掌握quarkus实现的一个CDI特性:装饰器(Decorator) 提到装饰器,熟悉设计模式的读者应该会想到装饰器模式,个人觉得下面这幅图很好的解释了装饰器

    2024年02月14日
    浏览(43)
  • quarkus依赖注入之六:发布和消费事件

    这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos 本文是《quarkus依赖注入》系列的第六篇,主要内容是学习事件的发布和接收 如果您用过Kafka、RabbitMQ等消息中间件,对消息的作用应该不会陌生,通过消息的订阅和发布可以降低系统之间的耦合性,

    2024年02月14日
    浏览(35)
  • quarkus依赖注入之五:拦截器(Interceptor)

    这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos 本文是《quarkus依赖注入》系列的第五篇,经过前面的学习,咱们熟悉了依赖注入的基本特性,接下来进一步了解相关的高级特性,先从本篇的拦截器开始 如果您熟悉spring的话,对拦截器应该不会陌

    2024年02月14日
    浏览(52)
  • quarkus依赖注入之十二:禁用类级别拦截器

    这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos 本篇是《quarkus依赖注入》系列的第十二篇,继续学习拦截器的另一个高级特性:禁用类级别拦截器 本篇由以下内容构成 编码验证类拦截器和方法拦截器的叠加效果 用注解 NoClassInterceptors 使类拦截器

    2024年02月13日
    浏览(38)
  • quarkus依赖注入之十三:其他重要知识点大串讲(终篇)

    这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos 本篇是《quarkus依赖注入》系列的终篇,前面十二篇已覆盖quarkus依赖注入的大部分核心内容,但依然漏掉了一些知识点,今天就将剩下的内容汇总,来个一锅端,轻松愉快的结束这个系列 总的来说,

    2024年02月13日
    浏览(34)
  • quarkus依赖注入之十一:拦截器高级特性上篇(属性设置和重复使用)

    这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos 本篇是《quarkus依赖注入》系列的第十一篇,之前的[《拦截器》]学习了拦截器的基础知识,现在咱们要更加深入的了解拦截器,掌握两种高级用法:拦截器属性和重复使用拦截器 先来回顾拦截器的基

    2024年02月13日
    浏览(37)
  • Springboot依赖注入Bean的三种方式,final+构造器注入Bean

    @Autowired注解的一大使用场景就是Field Injection。 通过Java的反射机制实现,所以private的成员也可以被注入具体的对象 优点 代码少,简洁明了。 新增依赖十分方便,不需要修改原有代码 缺点 容易出现空指针异常。Field 注入允许构建对象实例时依赖的对象为空,导致空指针异常

    2024年02月02日
    浏览(53)
  • 基于Xml方式Bean的配置-Bean的依赖注入以及·自动装配

    Bean的依赖注入方式 注入方式 配置方式 通过Bean的set方法注入 通过构造Bean的方法进行注入 其中,ref是reference的缩写形式,翻译为:涉及,参考的意思,用于引用其它Bean的id,value用于指定属性值 注入数据类型 普通数据类型:String、int、boolean,通过value属性指定 引用数据类型

    2024年02月07日
    浏览(55)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包