前2天在zipkin上面观察链路,发现一旦出现feign的远程调用,就会报SQL异常,用traceId到服务器上面搜索,又找不到错误日志,很是诡异。
然后排查了一下,翻了下代码也没找到什么头绪。最开始怀疑是logback输出日志级别不够,调整成了debug也不行。然后又怀疑是fegin调用的问题,也没什么收获。
后面仔细翻了下代码,发现代码是去记录操作日志,但是里面是用线程池处理的。类似如下
System.out.println("start................");
ExecutorService threadPoolExecutor = Executors.newFixedThreadPool(2);
threadPoolExecutor.submit(new Callable() {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
@Override
public Void call() throws Exception {
// todo: 日志记录
return null;
}
});
System.out.println("end................");
看到这段代码,乍一看没什么问题,但是为什么服务器上面没有错误日志,那很有可能就是丢失了,由于多线程?
验证一把,在call()方法中手动抛出异常,上面代码修改如下进行测试
System.out.println("start................");
ExecutorService threadPoolExecutor = Executors.newFixedThreadPool(2);
threadPoolExecutor.submit(new Callable() {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
@Override
public Void call() throws Exception {
// todo: 日志记录
throw new RuntimeException("抛出测试异常");
// return null;
}
});
System.out.println("end................");
写个测试用例跑一下,发现真的没有异常,那就看怎么才能打印出异常呢?再改一下代码
System.out.println("start................");
ExecutorService threadPoolExecutor = Executors.newFixedThreadPool(2);
threadPoolExecutor.execute(() -> {
// todo 记录日志
throw new RuntimeException("抛出测试异常");
});
System.out.println("end................");
写个测试用例跑一下,发现异常打印出来了
start................
end................
Exception in thread "pool-1-thread-1" java.lang.RuntimeException: 抛出测试异常
at com.example.threads.CallableTest.lambda$main$0(CallableTest.java:13)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)
那么问题就在于,Callable接口会封装异常,Runnable会直接抛出异常,更重要的是,这个场景没有正确的使用多线程...
后来又翻了一下《Java并发编程实战》,有这么一句,那么Callable和Runnable的区别也就出来了
具体的实现区别在后面吧,从源码分析一下
public Future submit(Callable task) {
if (task == null) throw new NullPointerException();
RunnableFuture ftask = newTaskFor(task);
execute(ftask);
return ftask;
}
protected RunnableFuture newTaskFor(Callable callable) {
return new FutureTask(callable);
}
submit添加任务会创建一个FutureTask任务
public FutureTask(Callable callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW; // ensure visibility of callable
}
会把刚才的Callable,赋予callable对象,用在后面执行执行
public void run() {
if (state != NEW ||
!UNSAFE.compareAndSwapObject(this, runnerOffset,
null, Thread.currentThread()))
return;
try {
Callable 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方法,里面会把异常放入outcome里面,所以在执行的过程中,调用处是拿不到异常的
protected void setException(Throwable t) {
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
outcome = t;
UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
finishCompletion();
}
}
那么,这个异常调用方,要怎么获取呢?玄妙就在get()里面
public V get() throws InterruptedException, ExecutionException {
int s = state;
if (s <= COMPLETING)
s = awaitDone(false, 0L);
return report(s);
}
看下report方法,如果是任务取消了,会抛出CancellationException。
如果是任务执行中有异常,会抛出ExecutionException
private V report(int s) throws ExecutionException {
Object x = outcome;
if (s == NORMAL)
return (V)x;
if (s >= CANCELLED)
throw new CancellationException();
throw new ExecutionException((Throwable)x);
}