单元测试多线程解决之道(代码片段)

lucare lucare     2022-11-22     301

关键词:

遇到问题

曾今在开发的过程遇到一个问题,当时有一个服务是群发邮件的,由于一次发送几十个上百个,所以就使用了多线程来操作。

在单元测试的时候,我调了这个方法测试下邮件发送,结果总是出现莫名其妙的问题,每次都没有全部发送成功。

后来我感觉到启动的子线程都被杀掉了,好像测试方法一走完就over了,试着在测试方法末尾让线程睡眠个几秒,结果就能正常发送邮件。

分析解决

感觉这个Junit有点猫腻,就上网查了一下,再跟踪下源码,果然发现了问题所在。

TestRunner的main方法:

public static void main(String[] args) 
    TestRunner aTestRunner = new TestRunner();

    try 
        TestResult r = aTestRunner.start(args);
        if (!r.wasSuccessful()) 
            System.exit(1);
        

        System.exit(0);
     catch (Exception var3) 
        System.err.println(var3.getMessage());
        System.exit(2);
    

上面显示了,不管成功与否,都会调用 System.exit() 方法关闭程序,这个方法是用来结束当前正在运行中的java虚拟机。

System.exit(0) 是正常退出程序,而 System.exit(1) 或者说非0表示非正常退出程序。

由此可见,junit 并不适合用来测试多线程程序呢,但是也不是没有方法,根据其原理可以尝试让主线程阻塞一下,等待其他子线程执行完毕再继续。

最简单的方法就是让主线程睡眠个几秒钟:

TimeUnit.SECONDS.sleep(5);

回顾复盘

除了让主线程睡眠以外,其实还有很多其他的工具可以帮我们解决这个问题。今天想起来了,就来试试吧。

来个数据库连接池相关的测试:

public class MultipleConnectionTest

    private HikariDataSource ds;


    @Before
    public void setup() 
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:mysql://127.0.0.1:3306/design");
        config.setDriverClassName("com.mysql.jdbc.Driver");
        config.setUsername("root");
        config.setPassword("fengcs");
        config.setMinimumIdle(1);
        config.setMaximumPoolSize(5);

        ds = new HikariDataSource(config);
    

    @After
    public void teardown() 
        ds.close();
    

    @Test
    public void testMulConnection() 

        ConnectionThread connectionThread = new ConnectionThread();
        Thread thread = null;
        for (int i = 0; i < 5; i++) 
            thread = new Thread(connectionThread, "thread-con-" + i);
            thread.start();
        

        // TimeUnit.SECONDS.sleep(5);  (1)
    

    private class ConnectionThread implements Runnable

        @Override
        public void run() 
            Connection connection = null;
            try 
                connection = ds.getConnection();
                Statement statement =  connection.createStatement();
                ResultSet resultSet = statement.executeQuery("select id from tb_user");
                String firstValue;
                System.out.println("<=============");
                System.out.println("==============>"+Thread.currentThread().getName() + ":");
                while (resultSet.next()) 
                    firstValue = resultSet.getString(1);
                    System.out.print(firstValue);
                
             catch (SQLException e) 
                e.printStackTrace();
             finally 
                try 
                    if (connection != null) 
                        connection.close();
                    
                 catch (SQLException e) 
                    e.printStackTrace();
                
            
        
    

这个代码一跑起来就会报错:

java.sql.SQLException: HikariDataSource HikariDataSource (HikariPool-1) has been closed.

1、使用 join 方法

根据上面的代码,直接加个 join 试试:

@Test
public void testMulConnection() 

    ConnectionThread connectionThread = new ConnectionThread();
    Thread thread = null;
    for (int i = 0; i < 5; i++) 
        thread = new Thread(connectionThread, "thread-con-" + i);
        thread.start();
        thread.join();
    

这样虽然可以成功执行,但仔细一看,和单个线程执行没有什么区别。对于主线程来说,start一个就join一个,开始阻塞等待子线程完成,然后循环开始第二个操作。

正确的操作应该类似这样:

Thread threadA = new Thread(connectionThread);
Thread threadB = new Thread(connectionThread);
threadA.start();
threadB.start();
threadA.join();
threadB.join();

这样多个线程可以一起执行。不过线程多了,这样写比较麻烦。

2、闭锁 - CountDownLatch

CountDownLatch 允许一个或多个线程等待其他线程完成操作。

CountDownLatch 的构造函数接收一个int类型的参数作为计数器,如果你想等待N个点完成,这里就传入N。

那么在这里,很明显主线程应该等待其他五个线程完成查询后再关闭。那么加上(1)和(2)处的代码,让主线程阻塞等待。

private static CountDownLatch latch = new CountDownLatch(5);  // (1)

@Test
public void testMulConnection() throws InterruptedException 

    ConnectionThread connectionThread = new ConnectionThread();
    Thread thread = null;
    for (int i = 0; i < 5; i++) 
        thread = new Thread(connectionThread, "thread-con-"+i);
        thread.start();
    

    latch.await();   // (2)


当我们调用CountDownLatch的countDown方法时,N就会减1,CountDownLatch的await方法
会阻塞当前线程,直到N变成零。增加(3)处代码,每个线程完成查询后就将计数器减一。

private class ConnectionThread implements Runnable

    @Override
    public void run() 
        Connection connection = null;
        try 
            connection = ds.getConnection();
            Statement statement =  connection.createStatement();
            ResultSet resultSet = statement.executeQuery("select id from tb_user");
            String firstValue;
            System.out.println("<=============");
            System.out.println("==============>"+Thread.currentThread().getName() + ":");
            while (resultSet.next()) 
                firstValue = resultSet.getString(1);
                System.out.print(firstValue);
            
            
            latch.countDown(); // (3)
         catch (SQLException e) 
            e.printStackTrace();
         finally 
            try 
                if (connection != null) 
                    connection.close();
                
             catch (SQLException e) 
                e.printStackTrace();
            
        
    

测试一下,完全满足要求。

3、栅栏- CyclicBarrier

CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一
组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会
开门,所有被屏障拦截的线程才会继续运行。

这里和 CountDownLatch 有所不同,但是主线程需要阻塞,依然在main方法末尾处加上一个同步点:

private static CyclicBarrier cyclicBarrier = new CyclicBarrier(6);  // (1)

@Test
public void testMulConnection() throws BrokenBarrierException, InterruptedException 

    ConnectionThread connectionThread = new ConnectionThread();
    Thread thread = null;
    for (int i = 0; i < 5; i++) 
        thread = new Thread(connectionThread, "thread-con-"+i);
        thread.start();
    

    cyclicBarrier.await();   // (2)


CyclicBarrier默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。

这个时候没有类似闭锁的 countDown 方法来计数,只能靠线程到达同步点来确认是否都到达,而其他线程不会走main方法的同步点,所以还需要一个其他五个线程汇合的同步点。那么可以在每个线程 run 方法末尾 await 一下:

private class ConnectionThread implements Runnable

    @Override
    public void run() 
        Connection connection = null;
        try 
            connection = ds.getConnection();
            Statement statement =  connection.createStatement();
            ResultSet resultSet = statement.executeQuery("select id from tb_user");
            String firstValue;
            System.out.println("<=============");
            System.out.println("==============>"+Thread.currentThread().getName() + ":");
            while (resultSet.next()) 
                firstValue = resultSet.getString(1);
                System.out.print(firstValue);
            
            
            cyclicBarrier.await();  // (3)
         catch (SQLException e) 
            e.printStackTrace();
         catch (InterruptedException e) 
            e.printStackTrace();
         catch (BrokenBarrierException e) 
            e.printStackTrace();
         finally 
            try 
                if (connection != null) 
                    connection.close();
                
             catch (SQLException e) 
                e.printStackTrace();
            
        
    

这样就感觉两者有一个潜在的通信机制,都到了就一起放开。只不过现在是六个线程参与计数了,CyclicBarrier 构造器传参应该是6(小于6也可能成功,大于6一定会一直阻塞)。

综合看了一下,我觉得最合适的还是 CountDownLatch。

这里主要是借单元测试多线程来加深下对并发相关知识点的理解,将其用于实践,来解决一些问题。关于这个单元测试多线程的问题很多人应该都知道,当初离职前面试过几个人,也问了这个问题,有几个说遇到过,我问为什么存在这个问题,你又是怎么解决的?结果没一个答得上来。

其实遇到问题是好事,都是成长的机会,每一个问题后面都隐藏着很多盲点,深挖下去一定收获颇多。

.net单元测试的艺术&单元测试之道c#版(代码片段)

目录1.单元测试概念2.单元测试的原则3.单元测试简单示例4.单元测试框架特性标签5.单元测试中的断言Assert6.单元测试中验证预期的异常7.单元测试中针对状态的间接测试8.单元测试在MVC模式中的实现8.单元测试相关参考9.示例源代... 查看详情

javaspringboottest单元测试中包括多线程时,没跑完就结束了(代码片段)

如何阻止JavaSpringBootTest单元测试中包括多线程时,没跑完就结束了使用CountDownLatchCountDownLatch、CyclicBarrier使用区别多线程ThreadPoolTaskExecutor应用JavaBasePooledObjectFactory对象池化技术@SpringBootTestpublicclassPoolTest@TestvoidbasePooledTest()throwsInte... 查看详情

京东多端全流程交易解决方案阿波罗平台ios单元测试实践(代码片段)

...何东西,而是来自测试文化的兴起。”——XML之父TimBray单元测试什么是单元测试?单元测试,是针对一小段代码或方法,检验被测代码一个小的、明确的功能是否正确,证明某段代码的行为和开发者期望一致的行为。简单来说... 查看详情

读《单元测试之道java版》

      读完这本单元测试之道,我们首先要知道什么是单元测试?为什么要使用单元测试?如何进行单元测试这些都是我们需要思考的。     单元测试是开发者编写的一小段代码,用于检验... 查看详情

java修炼之道--并发编程(代码片段)

...https://github.com/frank-lam/2019_campus_apply前言在本文将总结多线程并发编程中的常见面试题,主要核心线程生命周期、线程通信、并发包部分。主要分成“并发编程”和“面试指南”两部分,在面试指南中将讨论并发相关面经。参考... 查看详情

多线程(代码片段)

...内容每个程序中都必须有线程,因为线程是程序中的控制单元进程:是一个正在执行中的程序,每一个进程执行都有一个执行顺序,该顺序是一个执行路径,或者叫一个控制单元线程:就是进程中一个独立的控制单元,线程在控... 查看详情

单元测试之道读后感

  最近看了一下郑老师发的《单元测之道》这本书,读了之后我对CRRRECT边界条件,还有使用Mock对象,都有了初步的了解对单元测试有了很深刻的认识。这本书从什么是单元测试,为什么要使用单元测试这样的问题人手,以讨... 查看详情

用reacttestinglibrary和jest完成单元测试(代码片段)

...和稳定性了。接下来,让我们学习下,如何给React应用写单元测试吧??需要什么样的测试软件测试是有级别的,下面是《Google软件测试之道》一书中,对于测试认证级别的定义,摘录如下:级别1使用测试覆盖率工具。使用持续集... 查看详情

代码不要冗余之道-tdd三定律

提问什么是TDD三定律回答定律一、在编写不能通过的单元测试前,不可编译生产代码。定律二、只可编写刚好无法通过的单元测试,不能编译也算不过。定律三、只可编写刚好足以通过当前失败测试的生产代码。 查看详情

oo_2019_第二单元总结——多线程电梯(代码片段)

  传说中的多线程(魔鬼)电梯完成啦!一、程序设计分析与基于度量的程序结构分析  三次电梯都统一地采用了生产者-消费者模型,每次在前一次的基础上进行添加,没有大规模的重构,可以说设计含有一定的可拓展性... 查看详情

基于多线程任务队列执行时间测试——泛型单例模式落地(代码片段)

目录基于多线程任务队列执行时间测试——泛型单例模式落地1.需求2.遇到的问题3.解决思路4.具体代码4.1泛型单例4.2开始时间实体4.3实例化单例4.4获取任务结束时间5.小结5.1本文提供了单例模式实际应用中的一次落地;5.2单例模... 查看详情

单元测试,代码测试代码(代码片段)

#单元测试,代码测试代码针对函数、类,检测他的某个方面是否有问题的测试开发测试用例是一组单元测试,每个单元测试是一起核实函数和类在各种情况下的行为都符合要求为什么要做单元测试?1、单元测试->集成测试->2... 查看详情

springboot单元测试之druidnullpointexception问题解决(代码片段)

背景最近单元测试使用了spock框架。说实话,对于spock就是一小菜鸟。groovy语法基本靠猜。好不容跑起来了,却报了数据库连接从错误。错误信息java.lang.NullPointerExceptionatcom.alibaba.druid.support.http.WebStatFilter.doFilter(WebStatFilter.... 查看详情

linux提高:多线程压力测试(代码片段)

文章目录题目代码知识回顾线程线程特点题目创建一个多进程的程序,由用户输入进程个数和每个进程的运行圈数代码/*************************************************************************>FileName:main.c>Author:杨永利>Mail:1795018360@qq... 查看详情

linux提高:多线程压力测试(代码片段)

文章目录题目代码知识回顾线程线程特点题目创建一个多进程的程序,由用户输入进程个数和每个进程的运行圈数代码/*************************************************************************>FileName:main.c>Author:杨永利>Mail:1795018360@qq... 查看详情

Java 单元测试 - 并行化 + 多线程 + 无限次

】Java单元测试-并行化+多线程+无限次【英文标题】:JavaUnitTests-Parallelization+MultipleThreads+Infinitetimes【发布时间】:2018-02-2303:50:24【问题描述】:我有一堆JavaJUnit测试类,它们对ElasticSearch进行REST调用。我正在尝试检查编排测试套... 查看详情

《google软件测试之道》

...术语,简单按照测试范围去划分:小型测试:对一个代码单元的测试,通常就是单元测试中型测试:对两个或多个模块之间交互的测试,通常类比于“集成测试”大型测试:对一个应用系 查看详情

ssm单元测试时出现:failedtoloadapplicationcontext的一种可能解决办法(代码片段)

SSM单元测试时出现:严重:CaughtexceptionwhileallowingTestExecutionListener[org.springframewor[email protected]402bba4f]topreparetestinstance………………网上有很多相关错误的解决办法,但是没 查看详情