在上文中,配置kafka参数时因为参数类型与kafka预期的类型不匹配而导致其抛出了ConfigException。但很奇怪的是,在日志中看不到这个异常的任何信息,因此也导致我一直没往这方面考虑,最终通过一步步调试才发现对构造函数的调用居然没有返回,并经过进一步的分析搞清楚了是因为参数类型不匹配而抛出了异常,但为什么这个异常信息未打印出来的问题也一直困扰着我。
问题未搞清楚始终是块心病,因此对这块代码的调用关系又进行了跟踪,最终发现是在如下代码中进行初始化的:
ExecutorService e = Executors.newFixedThreadPool(consumerThreadCount);
for (int x = 0; x < consumerThreadCount; x++) {
e.submit(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
KafkaConsumerConfig kf = new KafkaConsumerConfig(topicName,
topicGroupId, topicServer,saveData, kafkaProperties);
KafkaConsumerConfig.consumerList.put(topicName, kf);
kf.run();
return true;
}
});
}
代码这块为什么使用线程池去处理,可能是业务方面的原因,暂且不去管它。但看到线程池突然想到,是否就是因为任务被提交到了线程池中执行而导致异常没有任何输出呢?
先验证下:
public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newFixedThreadPool(2);
executorService.submit(() -> {
System.out.println("test");
throw new Exception("test");
});
System.out.println("test");
executorService.shutdown();
}
发现果然没有任何输出!
这样问题原因就很清楚了。
但为什么在线程池中执行的任务抛出的异常不会被打印呢?
先来看下通过submit(Callable)方式,抛出异常时线程池如何处理。
具体跟踪过程很简单,最终执行的代码在FutureTask的run方法中:
public void run() {
if (state != NEW ||
!UNSAFE.compareAndSwapObject(this, runnerOffset,
null, Thread.currentThread()))
return;
try {
Callable<V> c = callable;
if (c != null && state == NEW) {
V result;
boolean ran;
try {
result = c.call();
ran = true;
} catch (Throwable ex) {
result = null;
ran = false;
setException(ex);
}
if (ran)
set(result);
}
} finally {
// runner must be non-null until state is settled to
// prevent concurrent calls to run()
runner = null;
// state must be re-read after nulling runner to prevent
// leaked interrupts
int s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}
可以看到,线程池将所有异常都捕获了,然后通过setException方法设置了异常到结果中;实际上这个结果通过get方法可以获取到。
而get方法在发现结果是异常时,会将其抛出!
因此,结论很明显:在线程池中提交Callable类型的任务时,异常将被线程池处理,并在调用Future的get方法时抛出。
也就是说,如果不调用get方法,那么异常是不会被处理的!这也是在日志中并未找到异常信息的原因。
至此,问题原因已经很清楚了。
再发散一下,如果提交的是Runnable的任何,会如何处理?
对线程池源代码进行分析,发现通过submit(Runnable)的方式提交的任务,最终执行的代码如下(ThreadPoolExecutor中):
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
while (task != null || (task = getTask()) != null) {
w.lock();
// If pool is stopping, ensure thread is interrupted;
// if not, ensure thread is not interrupted. This
// requires a recheck in second case to deal with
// shutdownNow race while clearing interrupt
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
beforeExecute(wt, task);
Throwable thrown = null;
try {
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
其中关键的是这一段:
try {
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown);
}
即在出现异常时抛出去了,同时会执行afterExecute方法。这里被抛出的异常在未进行处理,因此无法异常信息未打印出来。
但我们可以在afterExecute中处理这些异常,默认这个方法是空的。
因此,在使用线程池处理任务时,如果任务执行异常时不需要任何处理,并且即使没看到这些异常也无所谓,那么不需要特别注意的;否则,需要注意:
① 或者自行在Runnable或者Callable中处理异常;
② 或者,如果提交的是Runnable的任务,自己继承ThreadPoolExecutor类并覆盖其afterExecute方法,在其中将异常信息进行打印或者处理异常;
③ 或者,如果提交的是Callable的任务,那么需要调用其get方法来获取结果或者异常;并对异常进行处理;
④ 如果不需要知道任务的执行结果,那么建议提交Runnable而不是Callable的任务,同时通过①或者②的方式处理异常,否则按③的方式处理。