用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对象也是同样的道理,不管用没用,都要清理掉,避免线程变量污染。
有2种方法来解决page资源的关闭问题,一种是手动关闭,一种是自动关闭,猿哥回想大师说的方法,并改了下代码如下,
- 手动关闭的代码
@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,谁会去关注这种问题...)
- 自动关闭的代码
其实,还有一种方法,也很少人使用,我们不用显示的调用关闭来清除,我们看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查询结束,退出主线程"); }
猿哥改完上面的代码并运行了下,能够查询到所有的数据,并不会执行分页了,查询结果如下,哈哈,终于正常了,说明上面的改动是有效的
至此,猿哥在研究Mybatis分页插件PageHelper遇到的所有问题都迎刃而解了,也弄懂了Mybatis的分页对象的使用问题。这也侧面映射出,我们在使用ThreadLocal变量的时候也要格外小心,用完后需要清理掉。不然会发生各种奇奇怪怪的数据问题的。经过这次的问题,猿哥明白了在Java这条道路上继续砥砺前行是多么的不容易,不过,猿哥因祸得福,结识了一位大师(秃顶大师)。也拨开云雾见青天了,码痴猿哥稍作休整,又踏上了Java修炼之路。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
WEB-WORKER进阶学习(二)
由于JS单线程模型的原因,虽然可以通过异步来处理请求。但是最终还是需要由主线成处理 出于希望将渲染 / (请求、计算) 解耦的想法,所以对现在由axios构建的api请求层做改造,所有的数据请求交予web-work处理。达到渲染与请求分开的目的 问题 同时存在多少个Worker比较合适 ? 理论上worker没有上限,开启多少个都可以,根据实际情况即可。不建议按照CPU(navigator.hardwareConcurrency)核心数开启对应的数量 像目前做的请求/渲染分离就是开启4个作为守护线程。因为除了IE6,7最少支持4个并发请求 多个Worker如何协同工作 ? 需要考虑开启多个worker的统一调配的问题,与负载均衡的问题。 开启的worker可以通过数组存储 负载均衡可以使用轮询,最有可用等算法来处理 消息该如何处理 ? 与axios不同的是,worker处理请求跨越不同的线程,真正的实现异步的请求。那如何保证正确的触发回调 内部通过Promise构建,返回调用者Promise对象,同时为任务分配唯一ID。将任务ID, resolve,reject 同时存储记录 work...
- 下一篇
2020年 我要这样写代码
在 9102 年年初,一位室友问我一个问题,如何才能够提升写代码的能力? 可惜的是: 当时仅仅回复了一些自己的想法,如多看开源代码,多读书,多学习,多关注业界的动向与实践,同时也列了一些原则。但是这些并没有所总结,又或者说没有例子的语言始终是空泛的。所以在今年年底之际,对应着今年中遇到的形形色色的代码问题来一一讲解一下。 好代码的用处 实际上本书建立在一个相当不可靠的前提之上:好的代码是有意义的。我见过太多丑陋的代码给他们的主人赚着大把钞票,所以在我看来,软件要取得商业成功或者广泛使用,“好的代码质量”既不必要也不充分。即使如此,我仍然相信,尽管代码质量不能保证美好的未来,他仍然有其意义:有了质量良好的代码以后,业务需求能够被充满信心的开发和交付,软件用户能够及时调整方向以便应对机遇和竞争,开发团队能够再挑战和挫折面前保持高昂的斗志。总而言之,比起质量低劣,错误重重的代码,好的代码更有可能帮助用户取得业务上的成功。 以上文字摘抄于《实现模式》的前言,距离本书翻译已经时隔 10 年了,但是这本书仍旧有着很大的价值。同时对于上述言论,我并不持否认意见。但是我认为,坏代码比好代码更加的费财(...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS8安装MyCat,轻松搞定数据库的读写分离、垂直分库、水平分库
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- Docker安装Oracle12C,快速搭建Oracle学习环境
- SpringBoot2整合Redis,开启缓存,提高访问速度
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16
- SpringBoot2更换Tomcat为Jetty,小型站点的福音
- Eclipse初始化配置,告别卡顿、闪退、编译时间过长
- CentOS7安装Docker,走上虚拟化容器引擎之路
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果