△Hollis, 一个对Coding有着独特追求的人△

这是Hollis的第 362 篇原创分享

作者 l Hollis

来源 l Hollis(ID:hollischuang)

上周,因为要测试一个方法的在并发场景下的结果是不是符合预期,我写了一段单元测试的代码。写完之后截了个图发了一个朋友圈,很多人表示短短的几行代码,涉及到好几个知识点。

还有人给出了一些优化的建议。那么,这是怎样的一段代码呢?涉及到哪些知识,又有哪些可以优化的点呢?

让我们来看一下。

背景

先说一下背景,也就是要知道我们单元测试要测的这个方法具体是什么样的功能。我们要测试的服务是AssetService,被测试的方法是update方法。

update方法主要做两件事,第一个是更新Asset、第二个是插入一条AssetStream。

更新Asset方法中,主要是更新数据库中的Asset的信息,这里为了防止并发,使用了乐观锁。

插入AssetStream方法中,主要是插入一条AssetStream的流水信息,为了防止并发,这里在数据库中增加了唯一性约束

为了保证数据一致性,我们通过本地事务将这两个操作包在同一个事务中。

以下是主要的代码,当然,这个方法中还会有一些前置的幂等性校验、参数合法性校验等,这里就都省略了:

@Servicepublic class AssetServiceImpl implements AssetService {@Autowiredprivate TransactionTemplate transactionTemplate;@Overridepublic String update(Asset asset) {//参数检查、幂等校验、从数据库取出最新asset等。return transactionTemplate.execute(status -> {updateAsset(asset);return insertAssetStream(asset);});}}

因为这个方法可能会在并发场景中执行,所以该方法通过事务+乐观锁+唯一性约束做了并发控制。关于这部分的细节就不多讲了,大家感兴趣的话后面我再展开关于如何防并发的内容。

单测

因为上面这个方法是可能在并发场景中被调用的,所以需要在单测中模拟并发场景,于是,我就写了以下的单元测试的代码:

public class AssetServiceImplTest {private static ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("demo-pool-%d").build();private static ExecutorService pool = new ThreadPoolExecutor(20, 100,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>(128), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());@Autowiredprivate AssetService assetService;@Testpublic void test_updateConcurrent() {Asset asset = getAsset();//参数的准备//...//并发场景模拟CountDownLatch countDownLatch = new CountDownLatch(10);AtomicInteger atomicInteger =new AtomicInteger();            //并发批量修改,只有一条可以修改成功for (int i = 0; i < 10; i++) {pool.execute(() -> {try {String streamNo = assetService.update(asset);} catch (Exception e) {System.out.println("Error : " + e);failedCount.getAndIncrement();} finally {countDownLatch.countDown();}});}try {//主线程等子线程都执行完之后查询最新的资产countDownLatch.await();} catch (InterruptedException e) {e.printStackTrace();}Assert.assertEquals(failedCount.intValue(), 9);// 从数据库中反查出最新的Asset// 再对关键字段做注意校验}}

以上,就是我做了简化之后的单元测试的部分代码。因为要测并发场景,所以这里面涉及到了很多并发相关的知识。

很多人之前和我说,并发相关的知识自己了解的很多,但是好像没什么机会写并发的代码。其实,单元测试就是个很好的机会。

我们来看看上面的代码涉及到哪些知识点?

知识点

以上这段单元测试的代码中涉及到几个知识点,我这里简单说一下。

线程池

这里面因为要模拟并发的场景,所以需要用到多线程, 所以我这里使用了线程池,而且我没有直接用Java提供的Executors类创建线程池。

而是使用guava提供的ThreadFactoryBuilder来创建线程池,使用这种方式创建线程时,不仅可以避免OOM的问题,还可以自定义线程名称,更加方便的出错的时候溯源。(关于线程池创建的OOM问题)

CountDownLatch

因为我的单元测试代码中,希望在所有的子线程都执行之后,主线程再去检查执行结果。

所以,如何使主线程阻塞,直到所有子线程执行完呢?这里面用到了一个同步辅助类CountDownLatch。

用给定的计数初始化 CountDownLatch。由于调用了 countDown() 方法,所以在当前计数到达零之前,await 方法会一直受阻塞。

AtomicInteger

因为我在单测代码中,创建了10个线程,但是我需要保证只有一个线程可以执行成功。所以,我需要对失败的次数做统计。

那么,如何在并发场景中做计数统计呢,这里用到了AtomicInteger,这是一个原子操作类,可以提供线程安全的操作方法。

异常处理

因为我们模拟了多个线程并发执行,那么就一定会存在部分线程执行失败的情况。

因为方法底层没有对异常进行捕获。所以需要在单测代码中进行异常的捕获。

    try {String streamNo = assetService.update(asset);} catch (Exception e) {System.out.println("Error : " + e);failedCount.increment();} finally {countDownLatch.countDown();}

这段代码中,try、catch、finall都用上了,而且位置是不能调换的。失败次数的统计一定要放到catch中,countDownLatch的countDown也一定要放到finally中。

Assert

这个相信大家都比较熟悉,这就是JUnit中提供的断言工具类,在单元测试时可以用做断言。这就不详细介绍了。

优化点

以上代码涉及到了很多知识点,但是,难道就没有什么优化点了吗?

首先说一下,其实单元测试的代码对性能、稳定性之类的要求并不高,所谓的优化点,也并不是必要的。这里只是说讨论下,如果真的是要做到精益求精,还有什么点可以优化呢?

使用LongAdder代替AtomicInteger

我的朋友圈的网友@zkx 提出,可以使用LongAdder代替AtomicInteger。

java.util.concurrency.atomic.LongAdder是Java8新增的一个类,提供了原子累计值的方法。而且在其Javadoc中也明确指出其性能要优于AtomicLong。

首先它有一个基础的值base,在发生竞争的情况下,会有一个Cell数组用于将不同线程的操作离散到不同的节点上去(会根据需要扩容,最大为CPU核数,即最大同时执行线程数),sum()会将所有Cell数组中的value和base累加作为返回值。

核心的思想就是将AtomicLong一个value的更新压力分散到多个value中去,从而降低更新热点。所以在激烈的锁竞争场景下,LongAdder性能更好。

增加并发竞争

朋友圈网友 @Cafebabe 和 @普渡众生的面瘫青年 以及 @嘉俊 ,都提到同一个优化点,那就是如何增加并发竞争。

这个问题其实我在发朋友圈之前就有想到过,心中早已经有了答案,只不过有多位朋友能够几乎同时提到这一点还是很不错的。

我们来说说问题是什么。

我们为了提升并发,使用线程池创建了多个线程,想让多个线程并发执行被测试的方法。

但是,我们是在for循环中依次执行的,那么理论上这10次update方法的调用是顺序执行的。

当然,因为有CPU时间片的存在,这10个线程会争抢CPU,真正执行的过程中还是会发生并发冲突的。

但是,为了稳妥起见,我们还是需要尽量模拟出多个线程同时发起方法调用的。

优化的方法也比较简单,那就是在每一个update方法被调用之前都wait一下,直到所有的子线程都创建成功了,再开始一起执行。

这里就可以用到CyclicBarrier来实现,CyclicBarrier和CountDownLatch一样,都是关于线程的计数器。

CountDownLatch: 一个线程(或者多个), 等待另外N个线程完成某个事情之后才能执行。 

CyclicBrrier: N个线程相互等待,任何一个线程完成之前,所有的线程都必须等待。

所以,最终优化后的单测代码如下:

//主线程根据此CountDownLatch阻塞CountDownLatch mainThreadHolder = new CountDownLatch(10);//并发的多个子线程根据此CyclicBarrier阻塞CyclicBarrier cyclicBarrier = new CyclicBarrier(10);//失败次数计数器LongAdder failedCount = new LongAdder();//并发批量修改,只有一条可以修改成功for (int i = 0; i < 10; i++) {pool.execute(() -> {try {//子线程等待,所有线程就绪后开始执行cyclicBarrier.await();//调用被测试的方法String streamNo = assetService.update(asset);} catch (Exception e) {//异常发生时,对失败计数器+1System.out.println("Error : " + e);failedCount.increment();} finally {//主线程的阻塞器奇数-1mainThreadHolder.countDown();}});}try {//主线程等子线程都执行完之后查询最新的资产池计划mainThreadHolder.await();} catch (InterruptedException e) {e.printStackTrace();}//断言,保证失败9次,则成功一次Assert.assertEquals(failedCount.intValue(), 9);// 从数据库中反查出最新的Asset// 再对关键字段做注意校验

以上,就是关于我的一次单元测试的代码所涉及到的知识点,以及目前所能想到的相关的优化点。

第一次被公众号上近20万读者在线CodeReview,有点小小紧张。但是还是想问一下,对于这部分代码,你觉得还有什么可以优化的地方吗?

 

技术交流群

最近有很多人问,有没有读者交流群,想知道怎么加入。

最近我创建了一些群,大家可以加入。交流群都是免费的,只需要大家加入之后不要随便发广告,多多交流技术就好了。

目前创建了多个交流群,全国交流群、北上广杭深等各地区交流群、面试交流群、资源共享群等。

有兴趣入群的同学,可长按扫描下方二维码,一定要备注:全国 Or 城市 Or 面试 Or 资源,根据格式备注,可更快被通过且邀请进群。

▲长按扫描


往期推荐

别去外包

汇报下《Java工程师成神之路》的进展

学妹问我,并发问题的根源到底是什么?

如果你喜欢本文,

请长按二维码,关注 Hollis.

转发至朋友圈,是对我最大的支持。

点个 在看 

喜欢是一种感觉

在看是一种支持

↘↘↘

在线求CR,你觉得我这段Java代码还有优化的空间吗?相关推荐

  1. 你觉得我的这段Java代码还有优化的空间吗?

    上周,因为要测试一个方法的在并发场景下的结果是不是符合预期,我写了一段单元测试的代码.写完之后截了个图发了一个朋友圈,很多人表示短短的几行代码,涉及到好几个知识点. 还有人给出了一些优化的建议.那么, ...

  2. java代码上传exel,excle上传服务器并解析!求excel上传到服务器的java代码

    如何将数据上传给服务器 医嘱以形式发送过来? 办法有很多,最简单的,就是在机器里,建立2个数据库A,B,假如A是外务器. 在数据库中,以A数据库做发布,让B数据库订阅,弄好以后,A数据库的数据就会自动 ...

  3. 有趣的java代码_【有趣】这段java代码太古怪

    首先呢,来一段java代码来开点胃.等等等等,耍我呢,这是java代码? \u0070\u0075\u0062\u006c\u0069\u0063\u0020\u0063\u006c\u0061\u0 ...

  4. 怎么一键执行java程序_如何快速、低成本、低扰动地运行一段Java代码

    JVM是个运行服务端应用的好VM,但如果你只是想频繁地运行一段Java写的脚本,或者在跑一些辅助性的Java程序比如监控,比如日志收集,这时候的诉求就和平日里的应用不一样了: 1.启动快速,动静小. ...

  5. 一段java代码是如何执行的?

    原文:https://bbs.huaweicloud.com/blogs/250559 当你学会了java语言之后,你写了一些代码,然后你想要执行你的代码,来达成某些功能.那么,你都知道这段代码都是如 ...

  6. 如何执行一段java代码_V8 之 如何执行一段 JavaSscript 代码

    如何执行一段 JavaSscript 代码 解释执行 与 编译执行 解释执行,需要先将输入的源代码通过解析器编译成中间代码,之后直接使用解释器解释执行中间代码,然后直接输出结果. 编译执行.采用这种方 ...

  7. Java代码生成同一色系颜色_求大侠帮忙给这段JAVA代码 设置个背景颜色!

    换个颜色就成!importjava.awt.*;importjava.awt.event.*;importjavax.swing.*;importjava.sql.*;publicclassInque ...

  8. 一个类的java代码_求一段java代码,定义一个类

    你看看这样行不行:public class Test{ private String str; public Test(){} public Test(String str){ this.str =  ...

  9. iservice封装有哪些方法_请问这段Java代码能不能封装成一个方法

    问题描述 这段代码在我的项目中经常会被使用到,想要把它封装成一个方法以达到减少代码量的目的,但由于本人是个菜鸟没能做到,希望有心的大神提供下思路.在此先行拜谢了. 目的:想将hardwareServi ...

最新文章

  1. C语言——常见的字符串函数+内存操作函数的介绍及实现
  2. 在边缘,作为网关或在网格中构建控制平面以管理Envoy代理的指南
  3. anaconda如何更改环境配置_手把手教新手安装Anaconda配置开发环境
  4. PX4代码解析(5)
  5. SpringBoot项目集成Mybatis Plus(二)代码生成器
  6. 华为进军美国受挫:竟被美运营商巨头临时放鸽子
  7. 给大家送一个机械轴键盘~
  8. 天线方向图仿真(面阵、圆阵、圆环阵)matlab
  9. 分享到QQ空间——网站嵌入分享代码
  10. 【转】我那实现了自己理想的创业老公,却一毛钱股份都没有拿到
  11. iOS集成支付宝支付 Alipay
  12. BEV和Pseudo-Lidar
  13. 图像分割之分水岭分割算法
  14. 英语听力采用计算机化考试,北京英语听说考试2021年 北京英语听说机考满分
  15. wp模板里面的各种判断
  16. ADAMoracle预言机将数据传至链上实现区块链落地应用
  17. 软件开发新技术(工具及相关技术)
  18. 使IE浏览器支持webp格式图片显示
  19. 北师大计算机专业课代号,2020北京师范大学计算机改考,不是408
  20. 软件开发公司的提成制度【修订中】

热门文章

  1. python3主函数返回值_Python3
  2. linux nslookup 解析不到dns_涉及DNS的简单操作,只看这一篇就够了
  3. 数据湖,当然得要全闪存的!
  4. python用什么处理文件_利用Python如何快速处理文件
  5. python命令行输入函数回退_Anaconda--成功解决python2与python3之间随意切换的问题!...
  6. linux脚本里使用sftp,如何在shell脚本里使用sftp批量传送文件
  7. qtreewidgetitem 文字内存太长换行_table文字溢出显示省略号问题
  8. 三、Java面向对象编程有四个特征
  9. Linux C DNS 查询IP地址
  10. C/C++ 按行读取文件