Loading [MathJax]/jax/output/CommonHTML/config.js
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >在线求CR,你觉得我这段Java代码还有优化的空间吗?

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

作者头像
敖丙
发布于 2021-07-27 07:15:07
发布于 2021-07-27 07:15:07
95000
代码可运行
举报
文章被收录于专栏:三太子敖丙三太子敖丙
运行总次数:0
代码可运行

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

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

让我们来看一下。

背景

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

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

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

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

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@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);

        });

    }

}

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

单测

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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());

    @Autowired

    private AssetService assetService;

    @Test

    public 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,这是一个原子操作类,可以提供线程安全的操作方法。

异常处理

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    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个线程相互等待,任何一个线程完成之前,所有的线程都必须等待。

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//主线程根据此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) {

            //异常发生时,对失败计数器+1

            System.out.println("Error : " + e);

            failedCount.increment();

        } finally {

            //主线程的阻塞器奇数-1

            mainThreadHolder.countDown();

        }

    });

}

try {

    //主线程等子线程都执行完之后查询最新的资产池计划

    mainThreadHolder.await();

} catch (InterruptedException e) {

    e.printStackTrace();

}

//断言,保证失败9次,则成功一次

Assert.assertEquals(failedCount.intValue(), 9);

// 从数据库中反查出最新的Asset

// 再对关键字段做注意校验

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

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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-07-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 敖丙 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Java并发编程,看这篇就够了!
大家好!我是"无敌码农"。今天的文章将给大家分享Java并发编程相关的知识点,虽然类似的文章已有很多,但本文将以更贴近实际使用场景的方式进行阐述。具体将对Java常见的并发编程方式和手段进行总结,以便可以从使用角度更好地感知Java并发编程带来的效果,从而为后续更深入的理解Java并发机制进行铺垫。
用户5927304
2021/05/18
1.2K0
Java并发编程,看这篇就够了!
高并发业务必读 深入剖析 Java 并发包中的锁机制
小张和老李一起工作已有数月,双方在技术上也有了很多的交流,但是却总是存在一些争议。
李福春
2025/07/01
660
高并发业务必读 深入剖析 Java 并发包中的锁机制
java多线程高级教程,这些你都懂了吗?
一、countdownLatch和cyclicbarrier(这两个做多线程控制很好用,工作中会经常用到)
java架构师
2018/08/23
6490
java多线程高级教程,这些你都懂了吗?
突击并发编程JUC系列-并发工具 CountDownLatch
小伙伴们,大家好,我们又见面了,突击并发编程JUC系列实战并发工具发车了。新的章节还是新的故事讲解,不知各位小伙伴们在面试遇到CountDownLatch、CyclicBarrier、Semaphore和Exchanger工具类灵魂拷问。本章节提供一种并发流程控制的手段CountDownLatch。
山间木匠
2020/09/28
3800
突击并发编程JUC系列-并发工具 CountDownLatch
CountDownLatch和CyclicBarrier 傻傻的分不清?超长精美图文又来了
并发编程的三大核心是分工,同步和互斥。在日常开发中,经常会碰到需要在主线程中开启多个子线程去并行的执行任务,并且主线程需要等待所有子线程执行完毕再进行汇总的场景,这就涉及到分工与同步的内容了
用户4172423
2020/07/03
5200
CountDownLatch和CyclicBarrier 傻傻的分不清?超长精美图文又来了
Java中间件(2)--分布式系统&中间件从入门到精通(六)
上篇文章介绍了线程池使用的优点,synchronized和reentrantLock的区别,reentrantlock又有读写锁,适用于读多写少的场景,可以用tryLock获取锁,并且在构造器可以指定布尔值,来当做公平锁来使用。
keying
2022/07/29
3200
突击并发编程JUC系列-万字长文解密 JUC 面试题
CAS(Compare And Swap)指比较并交换。CAS算法CAS(V, E, N)包含 3 个参数,V 表示要更新的变量,E 表示预期的值,N 表示新值。在且仅在 V 值等于 E值时,才会将 V 值设为 N,如果 V 值和 E 值不同,则说明已经有其他线程做了更新,当前线程什么都不做。最后,CAS 返回当前 V 的真实值。Concurrent包下所有类底层都是依靠CAS操作来实现,而sun.misc.Unsafe为我们提供了一系列的CAS操作。
山间木匠
2020/10/26
4560
突击并发编程JUC系列-万字长文解密 JUC 面试题
JUC高并发编程详解
大家好,欢迎来到这篇关于JUC(Java Util Concurrent)高并发编程的博客!在这个数字时代,我们的软件需求越来越庞大,而对于高并发编程的需求也日益迫切。在Java的世界里,JUC就像一位强大的武士,为我们打开了处理并发编程的大门。今天,我们将深入了解JUC,学习它的各种武器和战术,助你在并发的战场上游刃有余。
繁依Fanyi
2024/02/04
3500
《Java面试题集中营》- Java并发
Jdk提供了stop()方法用于强制停止线程,但官方并不建议使用,因为强制停止线程会导致线程使用的资源,比如文件描述符、网络连接处于不正常的状态。建议使用标志位的方式来终止线程,如果线程中有使用无限期的阻塞方式,比如wait()没有设置超时时间,就只能使用interrupt()方法来终止线程
阿提说说
2024/07/14
1260
五分钟教会你JUC中的“CountDownLatch”和“CyclicBarrier”应该如何使用
JUC作为Java面试的必考板块,其重要性不言而喻。学习JUC包下的常用类不仅仅是在学习这些类怎么使用,更是在学习这些类中所蕴藏的设计思维。
程序员牛肉
2024/09/30
1400
五分钟教会你JUC中的“CountDownLatch”和“CyclicBarrier”应该如何使用
性能提升了200%!(优化篇)
最近不少运营同事找到我说:咱们的数据校对系统越来越慢了,要过很久才会显示出校对结果,你能不能快速优化一下呢?我:好的,我先了解下业务,后续优化下。
冰河
2022/06/15
3750
性能提升了200%!(优化篇)
并发编程面试必备:AQS 原理以及 AQS 同步组件总结
AQS的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包下面。
lyb-geek
2022/03/10
7340
并发编程面试必备:AQS 原理以及 AQS 同步组件总结
Java开发手册之并发处理
Executors返回的线程池对象的弊端如下: 1) FixedThreadPool和SingleThreadPool: 允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。 2) CachedThreadPool和ScheduledThreadPool: 允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。 5. 【强制】SimpleDateFormat 是线程不安全的类,一般不要定义为static变量,如果定义为 static,必须加锁,或者使用DateUtils工具类。 正例:注意线程安全,使用DateUtils。亦推荐如下处理:
全栈程序员站长
2022/08/04
3700
线程的三个同步器
日常中会有开启多个线程去并发执行任务,而主线程要等所有子线程执行完之后才能运行的需求。之前我们是使用Thread.join方法来实现的,过程如下:
晚上没宵夜
2020/04/13
5470
探索JAVA并发 - 同步工具类
闭锁的作用相当于一扇门,在这扇门没打开前,任何线程执行到这里都会被无情扣押,直到有人打开了这扇门,那些阻塞在门外的线程才会继续进行门后的流程。
acupt
2019/08/26
5070
Java 里如何实现线程间通信?
上篇介绍了Java垃圾回收机制,一文看懂Java垃圾回收机制, 本文来介绍Java多线程通讯的原理
开发者技术前线
2020/11/23
5500
Java 里如何实现线程间通信?
这些 Java 并发工具类,还有谁没用过?
比如:ConcurrentHashMap、AtomicInteger、Semaphore、CyclicBarrier、CountDownLatch、BlockingQueue 等等。
程序员鱼皮
2024/08/20
1870
这些 Java 并发工具类,还有谁没用过?
深入剖析Java中的CountDownLatch:同步协作的利器
CountDownLatch是一个同步工具类,它允许一个或多个线程等待其他线程完成操作。CountDownLatch用一个给定的计数器来初始化,该计数器的值表示需要等待完成的任务数量。每当一个线程完成其任务后,计数器的值就会减一。当计数器的值达到零时,表示所有需要等待的任务都已经完成,此时在CountDownLatch上等待的线程将被唤醒并可以继续执行。
公众号:码到三十五
2024/03/19
5760
深入剖析Java中的CountDownLatch:同步协作的利器
Java 并发编程实战详解:线程、线程池与锁机制
现代计算机大多是多核处理器,利用并发编程能提升程序吞吐量与响应速度。Java 提供了非常完善的并发工具集,涵盖从基础线程到高级并发类库,帮助开发者构建高性能、线程安全的程序。
用户11690571
2025/06/06
1450
Java 并发编程实战详解:线程、线程池与锁机制
我用这个同步工具类优化了一个人脸识别项目,启动时间从40分钟降到10分钟...
我将结合一个真实线上案例作为背景来展开讲解这一知识点。给大家讲清楚什么是同步工具类、适合的场景、解决了什么问题、各个实现方案的对比。希望对大家理解同步工具类这个知识点有所帮助。
陶朱公Boy
2022/10/28
2930
我用这个同步工具类优化了一个人脸识别项目,启动时间从40分钟降到10分钟...
推荐阅读
相关推荐
Java并发编程,看这篇就够了!
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档