欢迎访问我的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,但如果充值和扣费是在两个线程同时进行,而且各算各的,再分别用自己的计算结果去覆盖余额,最终会导致计算不准确

代码复现多线程同步问题

  • 咱们用代码来复现上图中的问题,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,如果不是则单元测试不通过
  • 执行单元测试,结果如下图,果然失败了

  • 来分析测试过程中的日志,有助于我们理解问题的原因,如下图,充值和扣费同时开始,充值先完成,此时余额是102,但是扣费无视102,依旧使用100作为余额去扣费,然后将扣费结果99写入余额,导致余额与正确的逻辑产生差距

  • 反复运行上述单元测试,可以发现每次得到的结果都不一样,这算是典型的多线程同步问题了吧...
  • 看到这里,经验丰富的您应该想到了多种解决方式,例如下面这五种都可以:
  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

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

  • 再来看看测试过程中的日志,如下图,之前的几个方法同时执行的情况已经消失了,每个方法在执行的时候,其他线程都在等待

  • 至此,bean锁知识点学习完毕,希望本篇能给您一些参考,为您的并发编程中添加新的方案

源码下载

名称 链接 备注
项目主页 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,如下图红框

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

学习路上,你不孤单,欣宸原创一路相伴...

quarkus依赖注入之九:bean读写锁的更多相关文章

  1. Spring的依赖注入和管理Bean

    采用Spring管理Bean和依赖注入 1.实例化spring容器 和 从容器获取Bean对象 实例化Spring容器常用的两种方式: 方法一: 在类路径下寻找配置文件来实例化容器 [推荐使用] Ap ...

  2. spring不依赖注入得到实体bean

    如题,我们一般用spring的ioc,通过配置注入接口得到这个实现类,现在通过研究公司平台框架发现还有一种方法得到spring文件配置的bean方法,举个例子(注:这个ApplicationConte ...

  3. springboot启动流程(九)ioc依赖注入

    所有文章 https://www.cnblogs.com/lay2017/p/11478237.html 正文 在前面的几篇文章中,我们多次提到这么一个转化过程: Bean配置 --> Bean ...

  4. spring-framework-中文文档三:依赖注入DI

    5.4依赖性 典型的企业应用程序不包含单个对象(或Spring的说法中的bean).即使最简单的应用程序也有几个对象一起工作来展示最终用户将其视为一个连贯的应用程序.下一节将介绍如何从定义许多独立的b ...

  5. 转载--浅谈spring4泛型依赖注入

    转载自某SDN-4O4NotFound Spring 4.0版本中更新了很多新功能,其中比较重要的一个就是对带泛型的Bean进行依赖注入的支持.Spring4的这个改动使得代码可以利用泛型进行进一步的 ...

  6. spring in action学习笔记一:DI(Dependency Injection)依赖注入之CI(Constructor Injection)构造器注入

    一:这里先说一下DI(Dependency Injection)依赖注入有种表现形式:一种是CI(Constructor Injection)构造方法注入,另一种是SI(Set Injection) ...

  7. Spring IOC源代码具体解释之容器依赖注入

    Spring IOC源代码具体解释之容器依赖注入 上一篇博客中介绍了IOC容器的初始化.通过源代码分析大致了解了IOC容器初始化的一些知识.先简单回想下上篇的内容 加载bean定义文件的过程.这个过程 ...

  8. Java Web系列:Spring依赖注入基础

    一.Spring简介 1.Spring简化Java开发 Spring Framework是一个应用框架,框架一般是半成品,我们在框架的基础上可以不用每个项目自己实现架构.基础设施和常用功能性组件,而是 ...

  9. Java Spring各种依赖注入注解的区别

    Spring对于Bean的依赖注入,支持多种注解方式: @Resource javax.annotation JSR250 (Common Annotations for Java) @Inject ...

  10. Spring源码解析三:IOC容器的依赖注入

    一般情况下,依赖注入的过程是发生在用户第一次向容器索要Bean是触发的,而触发依赖注入的地方就是BeanFactory的getBean方法. 这里以DefaultListableBeanFactory ...

随机推荐

  1. 文心一言 VS chatgpt (5)-- 算法导论2.2 3~4题

    三.再次考虑线性查找问题(参见练习 2.1-3).假定要查找的元素等可能地为数组中的任意元素,平均需要检查输入序列的多少元素?最坏情况又如何呢?用0记号给出线性查找的平均情况和最坏情况运行时间.证明你 ...

  2. 2021-02-06:假设字符串str长度为N,请问最长回文子串的长度是多少?

    福哥答案2021-02-06: 1.动态规划.无代码,见图.2.中心扩展法.无代码.3.Manacher算法.有代码,见图.1)理解回文半径数组.2)理解所有中心的回文最右边界R,和取得R时的中心点C ...

  3. 痞子衡嵌入式:MCUBootUtility v5.0发布,初步支持i.MXRT1180

    -- 痞子衡维护的NXP-MCUBootUtility工具距离上一个大版本(v4.0.0)发布过去4个多月了,期间痞子衡也做过两个小版本更新,但不足以单独介绍.这一次痞子衡为大家带来了全新大版本v5. ...

  4. Python相关镜像

    Python相关镜像 (1) pip使用说明 对于Python开发用户来讲,我们会经常使用pip安装软件包.但国外的源下载速度实在太慢,浪费时间且经常出现下载后安装出错问题.所以把PIP安装源替换成国 ...

  5. python学习之一 OS 文件夹的操作和文件操作

    # OS模块 :查看一个文件夹下所有文件,这个文件夹有文件夹,不能用walk# -- coding: UTF-8 --import osimport sys#C:\Users\Administrato ...

  6. BFF层聚合查询服务异步改造及治理实践 | 京东云技术团队

    首先感谢王晓老师的[接口优化的常见方案实战总结]一文总结,恰巧最近在对稳健理财BFF层聚合查询服务优化治理,针对文章内的串行改并行章节进行展开,分享下实践经验,主要涉及原同步改异步的过程.全异步化后衍 ...

  7. 解码器 | 基于 Transformers 的编码器-解码器模型

    基于 transformer 的编码器-解码器模型是 表征学习 和 模型架构 这两个领域多年研究成果的结晶.本文简要介绍了神经编码器-解码器模型的历史,更多背景知识,建议读者阅读由 Sebastion ...

  8. 效率神器,边看网页边问ChatGPT!神级ChatGPT插件(浏览器扩展)推荐!

    如果在看一个网页时,有些词不认识.句子不知道含义,怎么办? 憨憨版:不认识就算了呗,还能咋滴 进阶版:复制到 Google/Baidu 里问一问: AI达人版:复制到 ChatGPT/Claude 里 ...

  9. Redis6 的安装

    安装网址 ‍ Redis 官方网站 Redis 中文官方网站 http://redis.io http://redis.cn/ ‍ 安装版本 ‍ 6.2.1 for Linux(redis-6.2.1 ...

  10. vivo 自研鲁班分布式 ID 服务实践

    作者:vivo IT 平台团队- An Peng 本文介绍了什么是分布式ID,分布式ID的业务场景以及9种分布式ID的实现方式,同时基于vivo内部IT的业务场景,介绍了自研鲁班分布式ID服务的实践. ...