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

Posted Javachichi

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了你觉得我的这段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的问题,还可以自定义线程名称,更加方便的出错的时候溯源。

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一下,直到所有的子线程都创建成功了,再开始一起执行。

这就还可以用都到我们前面讲过的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
// 再对关键字段做注意校验 

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

最后,想问一下,对于这部分代码,你觉得还有什么可以优化的地方吗?

最后

一直想整理出一份完美的面试宝典,但是时间上一直腾不开,这套一千多道面试题宝典,结合今年金三银四各种大厂面试题,以及 GitHub 上 star 数超 30K+ 的文档整理出来的,我上传以后,毫无意外的短短半个小时点赞量就达到了 13k,说实话还是有点不可思议的。

一千道互联网 Java 工程师面试题

内容涵盖:Java、MyBatis、ZooKeeper、Dubbo、Elasticsearch、Memcached、Redis、mysql、Spring、SpringBoot、SpringCloud、RabbitMQ、Kafka、Linux等技术栈(485页)

初级—中级—高级三个级别的大厂面试真题

阿里云——Java 实习生/初级

List 和 Set 的区别 HashSet 是如何保证不重复的

HashMap 是线程安全的吗,为什么不是线程安全的(最好画图说明多线程环境下不安全)?

HashMap 的扩容过程

HashMap 1.7 与 1.8 的 区别,说明 1.8 做了哪些优化,如何优化的?

对象的四种引用

Java 获取反射的三种方法

Java 反射机制

Arrays.sort 和 Collections.sort 实现原理 和区别

Cloneable 接口实现原理

异常分类以及处理机制

wait 和 sleep 的区别

数组在内存中如何分配

答案展示:

美团——Java 中级

BeanFactory 和 ApplicationContext 有什么区别

Spring Bean 的生命周期

Spring IOC 如何实现

说说 Spring AOP

Spring AOP 实现原理

动态代理(cglib 与 JDK)

Spring 事务实现方式

Spring 事务底层原理

如何自定义注解实现功能

Spring MVC 运行流程

Spring MVC 启动流程

Spring 的单例实现原理

Spring 框架中用到了哪些设计模式

为什么选择 Netty

说说业务中,Netty 的使用场景

原生的 NIO 在 JDK 1.7 版本存在 epoll bug

什么是 TCP 粘包/拆包

TCP 粘包/拆包的解决办法

Netty 线程模型

说说 Netty 的零拷贝

Netty 内部执行流程

答案展示:

蚂蚁金服——Java 高级

题 1:

  1. jdk1.7 到 jdk1.8 Map 发生了什么变化(底层)?

  2. ConcurrentHashMap

  3. 并行跟并发有什么区别?

  4. jdk1.7 到 jdk1.8 java 虚拟机发生了什么变化?

  5. 如果叫你自己设计一个中间件,你会如何设计?

  6. 什么是中间件?

  7. ThreadLock 用过没有,说说它的作用?

  8. Hashcode()和 equals()和==区别?

  9. mysql 数据库中,什么情况下设置了索引但无法使用?

  10. mysql 优化会不会,mycat 分库,垂直分库,水平分库?

  11. 分布式事务解决方案?

  12. sql 语句优化会不会,说出你知道的?

  13. mysql 的存储引擎了解过没有?

  14. 红黑树原理?

题 2:

  1. 说说三种分布式锁?

  2. redis 的实现原理?

  3. redis 数据结构,使⽤场景?

  4. redis 集群有哪⼏种?

  5. codis 原理?

  6. 是否熟悉⾦融业务?记账业务?蚂蚁⾦服对这部分有要求。

好啦~展示完毕,大概估摸一下自己是青铜还是王者呢?

前段时间,在和群友聊天时,把今年他们见到的一些不同类别的面试题整理了一番,于是有了以下面试题集,也一起分享给大家~

如果你觉得这些内容对你有帮助,可以加入csdn进阶交流群,领取资料

基础篇


JVM 篇


MySQL 篇



Redis 篇




由于篇幅限制,详解资料太全面,细节内容太多,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!

需要的小伙伴,可以一键三连,下方获取免费领取方式!
在这里插入图片描述

以上是关于你觉得我的这段Java代码还有优化的空间吗?的主要内容,如果未能解决你的问题,请参考以下文章

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

关于缓存区溢出的这段经典代码为什么编译失败xvs

为啥我的这段python代码运行不出来结果,也没报错,跪求大神指点

为啥下面我写的这段代码老是提示"不允许使用不完整类型"

为啥使用类变量的这段代码不能像在 Java 中那样工作? [复制]

Android内存优化1 了解java内存分配 1