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

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

[[409316]]

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

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

让我们来看一下。

背景

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

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

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

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

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

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

  1. @Service  
  2. public class AssetServiceImpl implements AssetService {  
  3.     @Autowired  
  4.     private TransactionTemplate transactionTemplate;  
  5.     @Override  
  6.     public String update(Asset asset) {  
  7.         //参数检查、幂等校验、从数据库取出最新asset等。  
  8.         return transactionTemplate.execute(status -> {  
  9.             updateAsset(asset);  
  10.             return insertAssetStream(asset);  
  11.         });  
  12.     }  

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

单测

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

  1. public class AssetServiceImplTest {  
  2.     private static ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()  
  3.         .setNameFormat("demo-pool-%d").build();  
  4.     private static ExecutorService pool = new ThreadPoolExecutor(20, 100,  
  5.         0L, TimeUnit.MILLISECONDS,  
  6.         new LinkedBlockingQueue<Runnable>(128), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());  
  7.     @Autowired  
  8.     private AssetService assetService;  
  9.     @Test 
  10.      public void test_updateConcurrent() {  
  11.         Asset asset = getAsset();  
  12.         //参数的准备  
  13.         //... 
  14.          //并发场景模拟  
  15.         CountDownLatch countDownLatch = new CountDownLatch(10);  
  16.         AtomicInteger atomicInteger =new AtomicInteger();          
  17.          //并发批量修改,只有一条可以修改成功  
  18.         for (int i = 0; i < 10; i++) {  
  19.             pool.execute(() -> {  
  20.                 try {  
  21.                     String streamNo = assetService.update(asset);  
  22.                 } catch (Exception e) { 
  23.                      System.out.println("Error : " + e);  
  24.                     failedCount.getAndIncrement();  
  25.                 } finally {  
  26.                     countDownLatch.countDown();  
  27.                 }  
  28.             });  
  29.         }  
  30.         try {  
  31.             //主线程等子线程都执行完之后查询最新的资产  
  32.             countDownLatch.await();  
  33.         } catch (InterruptedException e) {  
  34.             e.printStackTrace();  
  35.         }  
  36.         Assert.assertEquals(failedCount.intValue(), 9);  
  37.         // 从数据库中反查出最新的Asset  
  38.         // 再对关键字段做注意校验  
  39.     }  

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

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

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

知识点

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

线程池

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

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

CountDownLatch

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

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

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

AtomicInteger

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

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

异常处理

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

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

  1. try {  
  2.       String streamNo = assetService.update(asset);  
  3.   } catch (Exception e) {  
  4.       System.out.println("Error : " + e);  
  5.       failedCount.increment();  
  6.   } finally {  
  7.       countDownLatch.countDown();  
  8.   } 

这段代码中,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个线程相互等待,任何一个线程完成之前,所有的线程都必须等待。

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

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

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

 

责任编辑:庞桂玉 来源: Hollis
相关推荐

2022-12-08 17:32:25

chatGPT人工智能聊天

2021-08-31 10:52:30

容量背包物品

2023-08-10 13:57:50

模型AI

2016-01-21 09:55:51

2022-10-09 09:38:10

高可用设计

2020-09-04 15:05:50

GitHub代码空间特定仓库

2023-04-14 10:40:45

工具编译器优化

2017-11-01 15:09:26

字体Android技术

2021-01-04 14:21:21

人工智能机器学习语言

2021-09-17 08:04:28

Hooks函数组件架构

2020-08-24 07:18:28

手机监听Facebook

2012-06-18 15:18:32

JS

2012-11-19 14:29:38

JavaJava异常异常处理

2021-08-10 10:48:39

拷贝代码架构耦合

2020-04-20 13:43:59

黑客联网攻击

2014-01-17 14:39:18

12306 抢票

2021-04-27 06:44:03

PythonCython编程语言

2021-01-29 08:09:32

Service接口表现层

2012-04-01 09:22:15

2012-04-01 10:47:47

点赞
收藏

51CTO技术栈公众号