你觉得我的这段Java代码还有优化的空间吗?
上周,因为要测试一个方法的在并发场景下的结果是不是符合预期,我写了一段单元测试的代码。写完之后截了个图发了一个朋友圈,很多人表示短短的几行代码,涉及到好几个知识点。
还有人给出了一些优化的建议。那么,这是怎样的一段代码呢?涉及到哪些知识,又有哪些可以优化的点呢?
让我们来看一下。
背景
先说一下背景,也就是要知道我们单元测试要测的这个方法具体是什么样的功能。我们要测试的服务是AssetService,被测试的方法是update方法。
update方法主要做两件事,第一个是更新Asset、第二个是插入一条AssetStream。
更新Asset方法中,主要是更新数据库中的Asset的信息,这里为了防止并发,使用了乐观锁。
插入AssetStream方法中,主要是插入一条AssetStream的流水信息,为了防止并发,这里在数据库中增加了唯一性约束。
为了保证数据一致性,我们通过本地事务将这两个操作包在同一个事务中。
以下是主要的代码,当然,这个方法中还会有一些前置的幂等性校验、参数合法性校验等,这里就都省略了:
@Service
public class AssetServiceImpl implements AssetService {
@Autowired
private TransactionTemplate transactionTemplate;
@Override
public 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(5, 100,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(128), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());
@Autowired
AssetService assetService;
@Test
public void test_updateConcurrent() {
Asset asset = getAsset();
//参数的准备
//...
//并发场景模拟
CountDownLatch countDownLatch = new CountDownLatch(10);
AtomicInteger failedCount =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 方法会一直受阻塞。(多线程中CountDownLatch的用法)
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一下,直到所有的子线程都创建成功了,再开始一起执行。
这就还可以用都到我们前面讲过的CountDownLatch。
所以,最终优化后的单测代码如下:
//主线程根据此CountDownLatch阻塞
CountDownLatch mainThreadHolder = new CountDownLatch(10);
//并发的多个子线程根据此CountDownLatch阻塞
CountDownLatch multiThreadHolder = new CountDownLatch(1);
//失败次数计数器
LongAdder failedCount = new LongAdder();
//并发批量修改,只有一条可以修改成功
for (int i = 0; i < 10; i++) {
pool.execute(() -> {
try {
//子线程等待,等待主线程通知后统一执行
multiThreadHolder.await();
//调用被测试的方法
String streamNo = assetService.update(asset);
} catch (Exception e) {
//异常发生时,对失败计数器+1
System.out.println("Error : " + e);
failedCount.increment();
} finally {
//主线程的阻塞器奇数-1
mainThreadHolder.countDown();
}
});
}
//通知所有子线程可以执行方法调用了
multiThreadHolder.countDown();
try {
//主线程等子线程都执行完之后查询最新的资产池计划
mainThreadHolder.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
//断言,保证失败9次,则成功一次
Assert.assertEquals(failedCount.intValue(), 9);
// 从数据库中反查出最新的Asset
// 再对关键字段做注意校验
以上,就是关于我的一次单元测试的代码所涉及到的知识点,以及目前所能想到的相关的优化点。
最后,还是想问一下,对于这部分代码,你觉得还有什么可以优化的地方吗?
你觉得我的这段Java代码还有优化的空间吗?的更多相关文章
- jsp页面:js方法里嵌套java代码(是操作数据库的),如果这个js 方法没被调用,当jsp页面被解析的时候,不管这个js方法有没有被调用这段java代码都会被执行?
jsp页面:js方法里嵌套java代码(是操作数据库的),如果这个js 方法没被调用,当jsp页面被解析的时候,不管这个js方法有没有被调用这段java代码都会被执行? 因为在解析时最新解析的就是JA ...
- java代码之美(11)---java代码的优化
java代码的优化 随着自己做开发时间的增长,越来越理解雷布斯说的: 敲代码要像写诗一样美.也能理解有一次面试官问我你对代码有洁癖吗? 一段好的代码会让人看就像诗一样,也像一个干净房间会让人看去很舒服 ...
- java代码(11) ---java代码的优化
java代码的优化 参考了一些Java开发手册有关代码的规范,觉得一段好的代码可以从三个维度去分析.1)性能,2)可扩展性,3)可读性 让我们看看别人是怎么去分析,还有值得我们去学习的地方,也是我正在 ...
- 35 个 Java 代码性能优化总结
前言 代码优化,一个很重要的课题.可能有些人觉得没用,一些细小的地方有什么好修改的,改与不改对于代码的运行效率有什么影响呢?这个问题我是这么考虑的,就像大海里面的鲸鱼一样,它吃一条小虾米有用吗?没用, ...
- Java 代码性能优化总结
前言 代码优化,一个很重要的课题.可能有些人觉得没用,一些细小的地方有什么好修改的,改与不改对于代码的运行效率有什么影响呢?这个问题我是这么考虑的,就像大海里面的鲸鱼一样,它吃一条小虾米有用吗?没用, ...
- Java代码性能优化总结
代码优化,一个很重要的课题.可能有些人觉得没用,一些细小的地方有什么好修改的,改与不改对于代码的运行效率有什么影响呢?这个问题我是这么考虑的,就像大海里面的鲸鱼一样,它吃一条小虾米有用吗?没用,但是, ...
- Java 代码性能优化
代码优化,一个很重要的课题.可能有些人觉得没用,一些细小的地方有什么好修改的,改与不改对于代码的运行效率有什么影响呢?这个问题我是这么考虑的,就像大海里面的鲸鱼一样,它吃一条小虾米有用吗?没用,但是, ...
- 小细节,大用途,35 个 Java 代码性能优化总结!
前言: 代码优化,一个很重要的课题.可能有些人觉得没用,一些细小的地方有什么好修改的,改与不改对于代码的运行效率有什么影响呢?这个问题我是这么考虑的,就像大海里面的鲸鱼一样,它吃一条小虾米有用吗?没用 ...
- 利用封装、继承对Java代码进行优化
注:本文实例分别可以在oldcastle(未优化的代码)和newcastle(优化后的代码)中查看,网址见文末 城堡游戏: 城堡中有多个房间,用户通过输入north, south, east, wes ...
随机推荐
- 痞子衡嵌入式:在SBL项目实战中妙用i.MXRT1xxx里SystemReset不复位的GPR寄存器
大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家介绍的是i.MXRT1xxx里SystemReset不复位的GPR寄存器的小妙用. 我们知道稍大规模的项目代码设计一般都是多人协作完成的,在项目 ...
- 快速上手 Linkerd v2 Service Mesh(服务网格)
在本指南中,我们将引导您了解如何将 Linkerd 安装到您的 Kubernetes 集群中. 然后我们将部署一个示例应用程序来展示 Linkerd 的功能. 安装 Linkerd 很容易.首先,您将 ...
- Vue的常用特性
Vue的常用特性 一.表单基本操作 都是通过v-model 单选框 1. 两个单选框需要同时通过v-model 双向绑定 一个值 2. 每一个单选框必须要有value属性 且value值不能一样 3. ...
- jQuery选择器中的特殊符号和关键字
一般情况下,在jQuery选择器中,我们很少会用到诸如"."."#"."("."["等特殊字符,因为根据W3C规定,HT ...
- Yolo:实时目标检测实战(上)
Yolo:实时目标检测实战(上) YOLO:Real-Time Object Detection 你只看一次(YOLO)是一个最先进的实时物体检测系统.在帕斯卡泰坦X上,它以每秒30帧的速度处理图像, ...
- 低层级GPU虚拟内存管理引论
低层级GPU虚拟内存管理引论 Introducing Low-Level GPU Virtual Memory Management CUDA应用程序越来越需要尽可能快速高效地管理内存.在CUDA 1 ...
- 软件工具将GPU代码迁移到fpga以用于AI应用
软件工具将GPU代码迁移到fpga以用于AI应用 Software tools migrate GPU code to FPGAs for AI applications 人工智能软件初创公司Mips ...
- Collection&Map
1.Collection 添加元素 boolean add(E e) 删除元素 boolean remove(E e) 元素个数 int size() 清空 void clear() 判空 boole ...
- centos 7查看系统网络情况netstat
查看系统网络情况 netstat ➢ 基本语法 netstat [选项] ➢ 选项说明 -an 按一定顺序排列输出 -p 显示哪个进程在调用 应用案例 请查看服务名为 sshd 的服务的信息. ➢ N ...
- huge page 能给MySQL 带来性能提升吗?
最近一直在做性能压测相关的事情,有公众号的读者朋友咨询有赞的数据库服务器有没有开启huge page,我听说过huge page会对性能有所提升,本文就一探究竟.对过程没有兴趣的可以直接看结论. 二 ...