上周因为想测试一个方法在并发场景下的结果是否符合预期,写了一段单元测试代码。写完后截图发到朋友圈。很多人说几行代码涉及几个知识点。其他人给出了一些优化建议。那么,这是什么样的代码呢?涉及到哪些知识点,可以优化哪些点?让我们来看看。背景先说一下背景,就是要知道我们单元测试要测试的方法是一个什么样的功能。我们要测试的服务是AssetService,要测试的方法是update方法。update方法主要做了两件事,第一是更新Asset,第二是插入一个AssetStream。在更新Asset的方法中,主要是更新数据库中的Asset信息。为了防止并发,这里使用了乐观锁。插入AssetStream的方法主要是插入一条AssetStream的管道信息。为了防止并发,在数据库中加入了唯一约束。为了保证数据的一致性,我们通过本地事务将这两个操作包装在同一个事务中。以下是主要代码。当然这个方法中会有一些前置幂等检查和参数有效性检查,这里省略:@ServicepublicclassAssetServiceImplimplementsAssetService{@AutowiredprivateTransactionTemplatetransactionTemplate;@OverridepublicStringupdate(Assetasset){//参数检查,幂等检查,从数据库等returntransactionTemplate.execute(status->{updateAsset(asset);returninsertAssetStream(asset);});}}因为这个方法可能会在并发场景下执行,所以这个方法通过事务+乐观锁+唯一来实现并发控制约束。这部分的细节我就不说了。如果大家有兴趣,后面我会展开关于如何防止并发的内容。单机测试因为上面的方法在并发场景下可能会被调用,所以需要在单机测试中模拟并发场景,所以写了如下单元测试代码:-%d").build();privatestaticExecutorServicepool=newThreadPoolExecutor(20,100,0L,TimeUnit.MILLISECONDS,newLinkedBlockingQueue(128),namedThreadFactory,newThreadPoolExecutor.AbortPolicy());@AutowiredprivateAssetServicerrassetService(vootpublicAssetServicerrasset){setasset()=getAs;//参数准备//...//并发场景模拟CountDownLatchcountDownLatch=newCountDownLatch(10);AtomicIntegeratomicInteger=newAtomicInteger();//并发批量修改,只有一个可以修改成功for(inti=0;i<10;i++){pool.execute(()->{try{StringstreamNo=assetService.update(asset);}catch(Exceptione){System.out.println("Error:"+e);failedCount.getAndIncrement();}finally{countDownLatch.countDown();}});}try{//主线程和其他子线程执行完毕后,查询最新的assetcountDownLatch.await();}catch(InterruptedExceptione){e.printStackTrace();}驴rt.assertEquals(failedCount.intValue(),9);//从数据库中取出最新的Asset//再次检查关键字段}}以上是我简化后单元测试的部分因为代码需要测试并发场景,涉及到很多并发相关的知识。之前很多人告诉我,他们对并发了解很多,但是他们似乎没有机会写并发代码。其实单元测试是一个很好的机会。我们来看看上面的代码涉及到哪些知识点?知识点上面的单元测试代码涉及到几个知识点,这里简单说一下。线程池需要使用多线程,因为需要模拟并发场景,所以我这里使用线程池,并没有直接使用Java提供的Executors类来创建线程池。而是使用guava提供的ThreadFactoryBuilder创建线程池。这样创建线程时,不仅可以避免OOM问题,还可以自定义线程名称,更容易追溯错误来源。(关于线程池造成的OOM问题)CountDownLatch是因为在我的单元测试代码中,希望子线程全部执行完毕后,主线程会检查执行结果。那么,如何让主线程阻塞直到所有子线程都执行完呢?这里使用了一个同步辅助类CountDownLatch。用给定的计数初始化CountDownLatch。由于调用了countDown()方法,所以await方法会阻塞,直到当前计数达到零。AtomicInteger是因为我在单个测试代码中创建了10个线程,但是我需要保证只有一个线程能够执行成功。所以,我需要计算失败的次数。那么,如何在并发场景下进行计数统计呢?这里使用的是AtomicInteger,它是一个原子操作类,可以提供线程安全的操作方法。异常处理因为我们模拟了多个线程并发执行,所以肯定会出现部分线程执行失败的情况。因为方法底层并没有捕获异常。所以在单元测试代码中捕获异常是很有必要的。try{StringstreamNo=assetService.update(asset);}catch(Exceptione){System.out.println("Error:"+e);failedCount.increment();}finally{countDownLatch.countDown();}这段代码当中他们,try,catch,final都用到了,位置不能调换。失败次数的统计必须放在catch中,countDownLatch的countDown也必须放在finally中。Assert相信大家都不陌生。这是JUnit中提供的断言工具类,可以作为单元测试中的断言。这个就不详细介绍了。优化点上面的代码涉及到很多知识点,但是没有优化点吗?首先声明一下,单元测试代码对性能和稳定性要求不高,所谓的优化点也没有必要。这只是为了讨论。如果真要做到精益求精,还有什么可以优化的呢?使用LongAdder代替AtomicInteger@zkx朋友圈的网友建议可以使用LongAdder代替AtomicInteger。java.util.concurrency.atomic.LongAdder是Java8中的一个新类,它提供了一个原子累加值的方法。并且在其Javadoc中也明确指出其性能优于AtomicLong。首先,它有一个基本的价值基础。在竞争的情况下,会有一个Cell数组,用于将不同线程的操作分散到不同节点(容量会根据需要扩展,最大为CPU核数,即最大同时执行数ofthreads),sum()会累加所有Cell数组中的值和基数作为返回值。核心思想是将AtomicLong的一个值的更新压力分散到多个值上,从而减少更新热点。所以LongAdder在锁竞争比较严重的场景下表现更好。增加并发竞争朋友圈的网友@Cafebabe、@普韦何生的头感邪恶青年和@嘉俊都提到了同一个优化点,那就是如何增加并发竞争。其实这个问题在发朋友圈之前我也想过,心里已经有了答案,但是很多朋友几乎同时提到这一点还是很不错的。让我们谈谈问题是什么。为了提高并发性,我们使用线程池创建了多个线程,我们希望多个线程并发执行被测试的方法。但是我们在for循环中是顺序执行的,所以理论上10次调用update方法是顺序执行的。当然,由于CPU时间片的存在,这10个线程会争夺CPU,在实际执行过程中还是会发生并发冲突。但是为了安全起见,我们还是需要模拟尽可能多的线程同时发起方法调用。优化方法也比较简单,就是在每次调用update方法之前都等待,直到所有的子线程都创建成功,然后开始一起执行。这里可以使用CyclicBarrier来实现。CyclicBarrier和CountDownLatch一样,是线程的计数器。CountDownLatch:一个线程(或多个),等待其他N个线程完成某事后再执行。CyclicBrrier:N个线程相互等待,所有线程必须等待任何一个线程完成。因此,最终优化后的单元测试代码如下://主线程根据这个CountDownLatch阻塞CountDownLatchmainThreadHolder=newCountDownLatch(10);//多个并发子线程根据这个CyclicBarrier阻塞CyclicBarriercyclicBarrier=newCyclicBarrier(10);//FailurecounterLongAdderfailedCount=newLongAdder();//并发批量修改,只有一个可以修改成功for(inti=0;i<10;i++){pool.execute(()->{try{//子线程等待,所有线程就绪后开始执行cyclicBarrier.await();//调用被测方法StringstreamNo=assetService.update(asset);}catch(Exceptione){//当异常发生时,失败计数器+1System.out.println("Error:"+e);failedCount.increment();}finally{//主线程的阻塞是奇数-1mainThreadHolder.countDown();}});}try{//主线程之后thread和其他子线程全部执行完毕,查询最新assetsPoolplanmainThreadHolder.await();}catch(InterruptedExceptione){e.printStackTrace();}//断言,保证失败9次,然后成功一次Assert.assertEquals(failedCount.intValue(),9);//从数据库中找出最新的Asset//查看关键字段。以上就是我单元测试的代码涉及到的知识点,以及目前能想到的相关优化点。