捕获线程池任务异常引发的血案

今天遇到一个很诡异的问题,在多款Android 4.2.2版本的手机上发现处理后台任务的服务对前台请求毫无响应。这里的后台服务是一个RemoteService,目的为了处理更新&上传等任务。本来以为是跨进程组件间通讯出现兼容性问题,后来根据分析发现问题没有那么简单,这里记录下问题原因。

第一个错误

在后台服务处理任务请求时,使用了自定义线程池处理异步任务,而Java 线程池java.util.concurrent.ThreadPoolExecutor会Catch住所有异常,即便是你运行例如下面的代码也不会抛出异常。

1
2
3
4
5
6
7
8
9
10
Runnable runnable = new Runnable() {
@Override
public void run() {
Test test = null;
System.out.println(test.toString());
}
};

mExecutor.submit(runnable);

在处理线程池异常捕获的问题时,犯下了第一个错误,使用了submit方法拿到任务执行结果会阻塞当前线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public Future<?> submit(Runnable task) {
Future<?> future = mExecutor.submit(task);
try {
future.get();
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // Reset interrupted status
} catch (ExecutionException e) {
Throwable exception = e.getCause();
// Forward to exception reporter
if (exception instanceof UncatchException) {
throw (UncatchException) exception;
}
}

return future;
}

第二个错误

在错误一的基础上,使用了一个开源并发库android-lite-go,先看它的异步任务是如何调度的。

1
2
3
4
5
6
7
8
9
10
11
12
13
int coreSize = CPU_CORE;
int queueSize = coreSize * 32;

synchronized (lock) {
if (runningList.size() < coreSize) {
runningList.add(scheduler);
threadPool.execute(scheduler);
} else if (waitingList.size() < queueSize) {
waitingList.addLast(scheduler);
} else {
//...
}
}
  • 当正在运行的任务数小于设定的coreSize时,submit的任务会提交到线程池。
  • coreSize由系统核心数确定。
  • 当正在运行的任务数小于queueSize时,submit的任务会提交到等待队列中。
  • 当一个任务执行完后,会从等待队列中获取一个任务提交给线程池执行。

在一个异步任务中使用阻塞当前线程的方法,把另一个任务提交给了executor。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private void runTaskOne() {
Runnable runnable = new Runnable() {
@Override
public void run() {
// do something
runTaskTwo();
}
};

mTaskController.submit(runnable);
}

private void runTaskTwo() {
Runnable runnable = new Runnable() {
@Override
public void run() {
// do task2
// ......
}
};

mTaskController.submit(runnable);
}

问题原因及解决方案

当正在运行的任务使用阻塞当前进程的submit方法提交另一个异步任务时,后提交的任务被放入等待队列等待线程池执行,但在Running list中的任务又在等待后任务执行的结果,这样就造成了死锁!!这个问题跟Android系统版本并没有关系,但因为Android 4.2.2版本的手机低端较多核心数较少,所以runningList的大小就比较小,Running List很容易就被填满。

解决方案

  1. 使用非阻塞的方法提交异步任务
  2. 自定义线程池,重写afterExecute()方法,在该中获取线程池运行的异步任务运行时异常。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(11, 100, 1, TimeUnit.MINUTES, //
new ArrayBlockingQueue<Runnable>(10000),//
new DefaultThreadFactory()) {

protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
printException(r, t);
}
};

private static void printException(Runnable r, Throwable t) {
if (t == null && r instanceof Future<?>) {
try {
Future<?> future = (Future<?>) r;
if (future.isDone())
future.get();
} catch (CancellationException ce) {
t = ce;
} catch (ExecutionException ee) {
t = ee.getCause();
} catch (InterruptedException ie) {
Thread.currentThread().interrupt(); // ignore/reset
}
}
if (t != null)
log.error(t.getMessage(), t);
}

参考文档

  1. 捕获Java线程池执行任务抛出的异常

转载请标明出处病已blog https://ivonhoe.github.io/