转载

用Junit测试多线程的正确姿势 原 荐

上回说到了猿哥(程序猿)遇到了Pagehelper分页问题,最后大师(架狗师)帮其解惑,猿哥茅塞顿开,对Mybatis的分页插件有了更加深入的了解。猿哥孜孜不倦对技术的渴望那是非常人所能及的,力求完全搞懂源码背后的原理。此时猿哥又脑洞大开,又要开始折腾自己了,想要写个测试代码来验证大师所说的那些技术问题。有趣的事情又发生了...

线程池模拟发起数据查询任务

首先,猿哥想到的是怎么样去模拟发起多次请求而且这2次请求还要分给同一个线程去执行,猿哥想到了JDK里面的线程池,于是二话不说,就开干,龙飞凤舞的写出了下面一段用Junit写的测试代码段如下,该代码段用了 ExecutorService 来创建一个固定的只有一个线程的线程池executor,然后写了2个任务,这2个任务一个只执行了 PageHelper.startPage(1,10); 分页操作,就没有继续数据库查询操作,另外一个是没有分页操作的查询全部数据的业务代码块,这样测试代码就准备好了,接下来就执行并验证本来没有执行分页的查询全部的数据最后返回的结果的条数。

@Test
    public void test() throws InterruptedException {
        ExecutorService executor = Executors.newFixedThreadPool(1);
        Runnable task = () -> {
            PageHelper.startPage(1,10);
        };
        executor.submit(task);
        Runnable task1 = () ->{
            List<AccessUserDto> list = userBusiness.selectAll();
            log.info("task1查询结果:"+list.size());
        };
        executor.submit(task1);
    }

新问题的出现

猿哥非常自信的用debug模式运行了上面的测试代码段,奇怪的事情发生了,程序并没有像之前预料的一样输出查询结果集总条数,现象就像task1任务并没有执行一样,猿哥甚是费解啊,为啥不能执行呢?猿哥第一反应是不是selectAll方法有问题,于是猿哥单独运行了这个 List<AccessUserDto> list = userBusiness.selectAll(); 语句,通过了单元测试,并能正常输出结果。既然查询没问题,猿哥接下来想到的是有可能是线程并没有正常执行导致结果未输出。猿哥对着这段代码,陷入了沉思当中,大约过了十几分钟,猿哥拍腿跳了起来,说:“靠,原来是这么回事,哈哈哈”(这猿哥已经写代码写到走火入魔的节奏了...)。

出现问题的根源所在

(猿哥在那自言自语...)

其实,问题很简单,是个简单的多线程问题,关系到主线程和子线程的销毁问题。我们可以看做Junit的test其实是个主线程,在主线程里面我们开了一个子线程去执行2次任务,当任务都提交后,子线程和主线程会同时的运行(宏观同时微观交替运行),但是如果子线程还没执行完,主线程先于子线程执行完并退出销毁,此时,子线程就也会被立即销毁,导致子线程执行到一半就挂逼了(老子都挂了,儿子怎么能独善其身呢也会随之而去也)。那要怎么改才能按自己的意愿执行呢,其实,有很多方法,下面介绍的一种,利用JDK里面的CountDownLatch类来实现线程之间的执行顺序。

Junit多线程测试的正确姿势

于是乎,猿哥又动手将之前的代码做了些改动,加入了同步器,来同步线程,代码如下,写完代码,猿哥开心的运行了代码,这次代码顺利执行,并打印出了结果。这种问题,说起来不是什么大问题,但是一旦发生了,也是很难查出来了,因为,你会发现每次debug,在不同的位置代码就执行不下去了,看似到处都有问题,其实就是主线程退出的时间点是变化的导致你debug到不同的代码段的时候子线程挂逼了。(Junit进行多线程测试的正确姿势要保证子线程顺利执行完后才能退出主线程...)

private final CountDownLatch countDownLatch = new CountDownLatch(2);

   @Test
    public void test() throws InterruptedException {
        ExecutorService executor = Executors.newFixedThreadPool(1);
        Runnable task = () -> {
            PageHelper.startPage(1,10);
            countDownLatch.countDown();
        };
        executor.submit(task);
        Runnable task1 = () ->{
            List<AccessUserDto> list = userBusiness.selectAll();
            log.info("task1查询结果:"+list.size());
            countDownLatch.countDown();
        };
        executor.submit(task1);
        countDownLatch.await();
        log.info("task1查询结束,退出主线程");
    }

Mybatis分页插件问题验证

上面的小问题解决了,回归正题,当猿哥运行上面的代码的时候,神奇的结果发生了,本不该分页的查询,却发生了分页查询,查询结果如下,惊呆了猿哥的双眼...确确实实的分页了,所以Pagehelper用的不好的时候是会引发很深很深的bug的,猿哥回想大师说的,PageHelper.startPage(1,10);创建的page对象其实是放在了ThreadLocal里面,所以我们用完分页资源一定要清空掉page对象,否则会长留在线程的本地变量中,等待下次查询的时候消费并清理掉。猿哥想,大师说过,Pagehelper的分页对象其实是一种资源,类似我们平时使用的文件流资源一样,使用完后要关闭的。其实page对象也是同样的道理,不管用没用,都要清理掉,避免线程变量污染。

用Junit测试多线程的正确姿势 原 荐

有2种方法来解决page资源的关闭问题,一种是手动关闭,一种是自动关闭,猿哥回想大师说的方法,并改了下代码如下,

  1. 手动关闭的代码
@Test
    public void test() throws InterruptedException {
        ExecutorService executor = Executors.newFixedThreadPool(1);
        Runnable task = () -> {
            try{
                PageHelper.startPage(1,10);
            } catch (Exception e) {
                
            } finally {
		//清理page对象
                PageHelper.clearPage();
            }
            countDownLatch.countDown();
        };
        executor.submit(task);
        Runnable task1 = () ->{
            List<AccessUserDto> list = userBusiness.selectAll();
            log.info("task1查询结果:"+list.size());
            countDownLatch.countDown();
        };
        executor.submit(task1);
        countDownLatch.await();
        log.info("task1查询结束,退出主线程");
    }

手动关闭很简单,类似我们获取文件流之后会在finally来关闭流一样,Pagehelper里面也有清除page分页对象的方法,直接使用即可。这样就能清理掉线程的本地变量。(MLGB,谁会去关注这种问题...)

  1. 自动关闭的代码

其实,还有一种方法,也很少人使用,我们不用显示的调用关闭来清除,我们看Page类,其实是实现了Closeable接口,而Closeable接口又实现了AutoCloseable接口,AutoCloseable接口有何神秘之处,我们看下他的介绍如下

An object that may hold resources (such as file or socket handles) until it is closed. The close() method of an AutoCloseable object is called automatically when exiting a try-with-resources block for which the object has been declared in the resource specification header. This construction ensures prompt release, avoiding resource exhaustion exceptions and errors that may otherwise occur.

意思就是如果用的姿势正确,我这个资源可以自动关闭的,即自动执行close()方法。这个正确姿势就是要用try-with-resources语法块来写,才会自动的执行close方法。try-with-resources到底什么来的,看下面的代码就知道了,是不是很简单,其实这语法块虽然不经常用,但是还挺好用的,预防我们未关闭资源导致的一些问题。(猿哥又在自言自语了...)

@Test
    public void test() throws InterruptedException {
        ExecutorService executor = Executors.newFixedThreadPool(1);
        Runnable task = () -> {
            try(Page ignored = PageHelper.startPage(1,10)){
                countDownLatch.countDown();
            } catch (Exception e){
                
            }
           
        };
        executor.submit(task);
        Runnable task1 = () ->{
            List<AccessUserDto> list = userBusiness.selectAll();
            log.info("task1查询结果:"+list.size());
            countDownLatch.countDown();
        };
        executor.submit(task1);
        countDownLatch.await();
        log.info("task1查询结束,退出主线程");
    }

猿哥改完上面的代码并运行了下,能够查询到所有的数据,并不会执行分页了,查询结果如下,哈哈,终于正常了,说明上面的改动是有效的 用Junit测试多线程的正确姿势 原 荐

至此,猿哥在研究Mybatis分页插件PageHelper遇到的所有问题都迎刃而解了,也弄懂了Mybatis的分页对象的使用问题。这也侧面映射出,我们在使用ThreadLocal变量的时候也要格外小心,用完后需要清理掉。不然会发生各种奇奇怪怪的数据问题的。经过这次的问题,猿哥明白了在Java这条道路上继续砥砺前行是多么的不容易,不过,猿哥因祸得福,结识了一位大师(秃顶大师)。也拨开云雾见青天了,码痴猿哥稍作休整,又踏上了Java修炼之路。

原文  https://my.oschina.net/wang5v/blog/3144558
正文到此结束
Loading...