使用CompletableFuture+ExecutorService+Logback的多线程测试
1. 环境
Java: jdk1.8.0_144
2. 背景
Java多线程执行任务时,Logback输出的主线程和各个子线程的业务日志需要区分时,可以根据线程池和执行的线程来区分,但若要把它们联系起来只能根据时间线,既麻烦又无法保证准确性。
2018-10-27 23:09:22 [INFO][com.lxp.tool.log.LogAndCatchExceptionRunnableTest][main][testRun][38] -> test start
2018-10-27 23:09:22 [INFO][com.lxp.tool.log.RunnabeTestHelper][pool-1-thread-1][lambda$getRunnable$0][16] -> This is runnable.
2018-10-27 23:09:22 [INFO][com.lxp.tool.log.RunnabeTestHelper][pool-1-thread-2][lambda$getRunnable$0][16] -> This is runnable.
2018-10-27 23:09:22 [INFO][com.lxp.tool.log.RunnabeTestHelper][pool-1-thread-1][lambda$getRunnable$0][16] -> This is runnable.
2018-10-27 23:09:22 [INFO][com.lxp.tool.log.RunnabeTestHelper][pool-1-thread-2][lambda$getRunnable$0][16] -> This is runnable.
2018-10-27 23:09:22 [INFO][com.lxp.tool.log.RunnabeTestHelper][pool-1-thread-1][lambda$getRunnable$0][16] -> This is runnable.
2018-10-27 23:09:22 [INFO][com.lxp.tool.log.LogAndCatchExceptionRunnableTest][main][testRun][48] -> test finish
org.slf4j.MDC类提供了一个极好的解决方案,它可以为各个线程设置独有的上下文,当有必要时也可以把主线程的上下文复制给子线程,此时子线程可以拥有主线程+子线程的信息,在子线程退出前恢复到主线程上下文,如此一来,日志信息可以极大地便利定位问题,org.slf4j.MDC类在线程上下文切换上的应用记录本文的目的之一。
另一个则是过去一直被自己忽略的多线程时退出的问题,任务需要多线程执行有两种可能场景
- 多个任务互相独立,某个任务失败并不应该影响其它的任务继续执行
- 多个子任务组成一个完整的主任务,若某个子任务失败它应该直接退出,不需要等所有子任务完成
3. org.slf4j.MDC类在线程上下文切换时的应用
3.1 实现包装线程
- AbstractLogWrapper
public class AbstractLogWrapper<T> {
private final T job;
private final Map<?, ?> context;
public AbstractLogWrapper(T t) {
this.job = t;
this.context = MDC.getCopyOfContextMap();
}
public void setLogContext() {
if (this.context != null) {
MDC.setContextMap(this.context);
}
}
public void clearLogContext() {
MDC.clear();
}
public T getJob() {
return this.job;
}
}
- LogRunnable
public class LogRunnable extends AbstractLogWrapper<Runnable> implements Runnable {
public LogRunnable(Runnable runnable) {
super(runnable);
}
@Override
public void run() {
// 把主线程上下文复到子线程
this.setLogContext();
try {
getJob().run();
} finally {
// 恢复主线程上下文
this.clearLogContext();
}
}
}
- LogAndCatchExceptionRunnable
public class LogAndCatchExceptionRunnable extends AbstractLogWrapper<Runnable> implements Runnable {
private static final Logger LOGGER = LoggerFactory.getLogger(LogAndCatchExceptionRunnable.class);
public LogAndCatchExceptionRunnable(Runnable runnable) {
super(runnable);
}
@Override
public void run() {
// 把主线程上下文复到子线程
this.setLogContext();
try {
getJob().run();
} catch (Exception e) { // Catch所有异常阻止其继续传播
LOGGER.error(e.getMessage(), e);
} finally {
// 恢复主线程上下文
this.clearLogContext();
}
}
}
3.2 配置%X
输出当前线程相关联的NDC
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="stdot" class="ch.qos.logback.core.ConsoleAppender">
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%p][%c][%t][%M][%L] %replace(Test_Method=%X{method} runn-able=%X{runn_able}){'.+=( |$)', ''} -> %m%n</pattern>
</layout>
</appender>
<root level="debug">
<appender-ref ref="stdot"/>
</root>
</configuration>
3.3 配置线程相关信息并测试
class RunnabeTestHelper {
private static final Logger LOGGER = LoggerFactory.getLogger(RunnabeTestHelper.class);
private static final String RUNNABLE = "runn_able";
static Runnable getRunnable() {
return () -> {
MDC.put(RUNNABLE, String.valueOf(System.currentTimeMillis()));
LOGGER.info("This is runnable.");
};
}
}
- 测试方法
@Test
public void testRun() {
try {
MDC.put("method", "testRun");
LOGGER.info("test start");
LogAndCatchExceptionRunnable logRunnable = spy(new LogAndCatchExceptionRunnable(RunnabeTestHelper.getRunnable()));
Set<String> set = new HashSet<>();
doAnswer(invocation -> set.add(invocation.getMethod().getName())).when(logRunnable).setLogContext();
doAnswer(invocation -> set.add(invocation.getMethod().getName())).when(logRunnable).clearLogContext();
List<CompletableFuture<Void>> futures = IntStream.rangeClosed(0, 4).mapToObj(index -> CompletableFuture.runAsync(logRunnable, executorService)).collect(Collectors.toList());
futures.forEach(CompletableFuture::join);
assertEquals("[setLogContext, clearLogContext]", set.toString());
LOGGER.info("test finish");
} finally {
MDC.clear();
}
}
- 测试结果
2018-11-01 01:08:04 [INFO][com.lxp.tool.log.LogRunnableTest][main][testRun][41] -> test start
2018-11-01 01:08:05 [INFO][com.lxp.tool.log.RunnabeTestHelper][pool-1-thread-1][lambda$getRunnable$0][16] Test_Method=testRun runn-able=1541005685003 -> This is runnable.
2018-11-01 01:08:05 [INFO][com.lxp.tool.log.RunnabeTestHelper][pool-1-thread-1][lambda$getRunnable$0][16] Test_Method=testRun runn-able=1541005685004 -> This is runnable.
2018-11-01 01:08:05 [INFO][com.lxp.tool.log.RunnabeTestHelper][pool-1-thread-1][lambda$getRunnable$0][16] Test_Method=testRun runn-able=1541005685004 -> This is runnable.
2018-11-01 01:08:05 [INFO][com.lxp.tool.log.RunnabeTestHelper][pool-1-thread-2][lambda$getRunnable$0][16] Test_Method=testRun runn-able=1541005685003 -> This is runnable.
2018-11-01 01:08:05 [INFO][com.lxp.tool.log.RunnabeTestHelper][pool-1-thread-2][lambda$getRunnable$0][16] Test_Method=testRun runn-able=1541005685005 -> This is runnable.
2018-11-01 01:08:05 [INFO][com.lxp.tool.log.LogRunnableTest][main][testRun][50] -> test finish
4. 多线程执行子线程出现异常时的处理
class RunnabeTestHelper {
private static final Logger LOGGER = LoggerFactory.getLogger(RunnabeTestHelper.class);
static Runnable getRunnable(AtomicInteger counter) {
return () -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
LOGGER.error(e.getMessage(), e);
}
if (counter.incrementAndGet() == 2) {
throw new NullPointerException();
}
LOGGER.info("This is {} runnable.", counter.get());
};
}
static Runnable getRunnableWithCatchException(AtomicInteger counter) {
return () -> {
try {
Thread.sleep(1000);
if (counter.incrementAndGet() == 2) {
throw new NullPointerException();
}
LOGGER.info("This is {} runnable.", counter.get());
} catch (Exception e) {
LOGGER.error("error", e);
}
};
}
}
4.1 选择一:放充执行未执行的其它子线程
- 调用LogRunnable,允许子线程的异常继续传播
@Test
public void testRunnableWithoutCatchException() {
Logger logger = Mockito.mock(Logger.class);
AtomicInteger counter = new AtomicInteger(0);
List<CompletableFuture<Void>> futures = IntStream.rangeClosed(0, 4).mapToObj(index -> CompletableFuture.runAsync(new LogRunnable(RunnabeTestHelper.getRunnable(counter)), executorService)).collect(Collectors.toList());
try {
futures.forEach(CompletableFuture::join);
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
// 由于子线程的异常导致主线程退出,并不是所有任务都得到执行机会
assertEquals(2, counter.get());
verify(logger, Mockito.times(1)).error(anyString(), any(Throwable.class));
}
4.2 选择二:执行完所有无异常的子线程
- 调用LogRunnable,在线程内部阻止异常扩散
@Test
public void testRunnableWithCatchException() {
AtomicInteger counter = new AtomicInteger(0);
List<CompletableFuture<Void>> futures = IntStream.rangeClosed(0, 4).mapToObj(index -> CompletableFuture.runAsync(new LogRunnable(RunnabeTestHelper.getRunnableWithCatchException(counter)), executorService)).collect(Collectors.toList());
futures.forEach(CompletableFuture::join);
// 由于子线程的异常被阻止,所有线程都得到执行机会
assertEquals(5, counter.get());
}
- 调用LogAndCatchExceptionRunnable,在包装类阻止异常扩散
@Test
public void testRunnableWithoutCatchException() {
AtomicInteger counter = new AtomicInteger(0);
List<CompletableFuture<Void>> futures = IntStream.rangeClosed(0, 4).mapToObj(index -> CompletableFuture.runAsync(new LogAndCatchExceptionRunnable(RunnabeTestHelper.getRunnable(counter)), executorService)).collect(Collectors.toList());
futures.forEach(CompletableFuture::join);
// 由于子线程的异常被阻止,所有线程都得到执行机会
assertEquals(5, counter.get());
}
@Test
public void testRunnableWithCatchException() {
AtomicInteger counter = new AtomicInteger(0);
List<CompletableFuture<Void>> futures = IntStream.rangeClosed(0, 4).mapToObj(index -> CompletableFuture.runAsync(new LogAndCatchExceptionRunnable(RunnabeTestHelper.getRunnableWithCatchException(counter)), executorService)).collect(Collectors.toList());
futures.forEach(CompletableFuture::join);
// 由于子线程的异常被阻止,所有线程都得到执行机会
assertEquals(5, counter.get());
}
使用CompletableFuture+ExecutorService+Logback的多线程测试的更多相关文章
- Junit使用GroboUtils进行多线程测试
写过Junit单元测试的同学应该会有感觉,Junit本身是不支持普通的多线程测试的,这是因为Junit的底层实现上,是用System.exit退出用例执行的.JVM都终止了,在测试线程启动的其他线程自 ...
- ExecutorService 建立一个多线程的线程池的步骤
ExecutorService 建立一个多线程的线程池的步骤: 线程池的作用: 线程池功能是限制在系统中运行的线程数. 依据系统的环境情况,能够自己主动或手动设置线程数量.达到执行的最佳效果:少了浪费 ...
- testng入门教程12 TestNG执行多线程测试
testng入门教程 TestNG执行多线程测试 testng入门教程 TestNG执行多线程测试 并行(多线程)技术在软件术语里被定义为软件.操作系统或者程序可以并行地执行另外一段程序中多个部分或者 ...
- 关于JUnit4无法支持多线程测试的解决方法
转自:https://segmentfault.com/a/1190000003762719 其实junit是将test作为参数传递给了TestRunner的main函数.并通过main函数进行执行. ...
- TestNG多线程测试-注解方式实现
用@Test(invocationCount = x,threadPoolSize = y)声明,invocationCount表示执行次数,threadPoolSize表示线程池大小. packag ...
- TestNg之XMl形式实现多线程测试
为什么要使用多线程测试? 在实际测试中,为了节省测试时间,提高测试效率,在实际测试场景中经常会采用多线程的方式去执行,比如爬虫爬数据,多浏览器并行测试. 关于多线程并行测试 TestNG中实现多线程并 ...
- JUNIT4 GroboUtils多线程测试
阅读更多 利用JUNIT4,GroboUtils进行多线程测试 多线程编程和测试一直是比较难搞的事情,特别是多线程测试.只用充分的测试,才可以发现多线程编码的潜在BUG.下面就介绍一下我自己在测试多线 ...
- TestNG 多线程测试
TestNG以注解的方式实现多线程测试 import org.testng.annotations.Test; public class TreadDemo { // invocationCount ...
- 【多线程】java多线程 测试例子 详解wait() sleep() notify() start() join()方法 等
java实现多线程,有两种方法: 1>实现多线程,继承Thread,资源不能共享 2>实现多线程 实现Runnable接口,可以实现资源共享 *wait()方法 在哪个线程中调用 则当前 ...
随机推荐
- gdb源码安装过程中的问题:no termcap library found
gdb使用源码安装的时候遇到错误:no termcap library found ./configure --> make --> make install 解决办法,下载termca ...
- 【Todo】git的fast forward & git命令学习 & no-ff
git的fast-forward在之前的文章有介绍过,但是介绍的不细: http://www.cnblogs.com/charlesblc/p/5953066.html fast-forward方式就 ...
- 高仿微信实现左滑显示删除button功能
在实际项目中删除列表中的某一项是很常见的功能.传统的做法能够使用长按监听器等,而如今流行的做法是左滑弹出删除button,微信,QQ等都是这么做的,以下做一个演示样例,代码例如以下: 主页面MainA ...
- 与linux相处的日子里
在前几天装了一下linux操作系统,并安装了几个经常使用的工具.如今就谈谈我的感受吧! 对一个连linux几个字母都不会拼写的人来说.让我參与这个工作可谓是:"太残忍啦!"当然这在 ...
- C#.net word excel powerpoint (ppt) 转换成 pdf 文件
using System;using System.Collections.Generic;using System.ComponentModel;using System.Data;using Sy ...
- Oracle创建索引的原则(转)
Oracle 建立索引及SQL优化 数据库索引: 索引有单列索引复合索引之说 如何某表的某个字段有主键约束和唯一性约束,则Oracle 则会自动在相应的约束列上建议唯一索引.数据库索引主要进行提高访问 ...
- Maple入门使用教程
http://anony3721.blog.163.com/blog/static/51197420105173915247/ 命令的运行:1.每条命令必须用":"(运行后不显示) ...
- JOIN ,LEFT JOIN ,ALL JOIN 等的区别和联系
left join(左联接) 返回包括左表中的所有记录和右表中联结字段相等的记录 right join(右联接) 返回包括右表中的所有记录和左表中联结字段相等的记录 inner join(等值连接) ...
- 跨平台C++:(前言)正确打开C++的方式
接触C++已经十五年了...但是对于C++而言,我至今是个门外汉,不是谦虚,而是确实不得其门而入. 历程是这样的—— 大学考研要考C++,就自学了.研没考上,C++算是学了,准确的说是C++的语法,以 ...
- scala快速学习笔记(二):控制结构,类和对象
IV.控制结构 1.if/else 除基本用法外,if/else语句能用来赋值,进而代替?:运算符.这得益于在Scala中,每个语句块都有值,就是该语句块最后一个语句的值.请看下面的代码. def a ...