多线程下嵌套异步任务导致程序假死问题
作者:xiaolyuh123
问题描述
线上环境异步任务全部未执行,代码没有抛出任何异常和提示,CPU、内存都很正常,基本没有波动,GC也没啥异常的。
问题原因
经定位是异步由于嵌套异步任务使用了Future.get()
方法导致的程序阻塞
手动使用线程池示例
public class FutureBlockTest { public static void main(String[] args) { // 为了模拟我这里只存创建一个工作线程 ExecutorService fixedThreadPool = Executors.newFixedThreadPool(1); // 第一层异步任务 Runnable runnable = () -> { System.out.println(Thread.currentThread().getName() + "-main-thread"); // 第二层异步任务(嵌套任务) FutureTask<Long> futureTask = new FutureTask<>(() -> { System.out.println(Thread.currentThread().getName() + "-child-thread"); return 10L; }); fixedThreadPool.execute(futureTask); System.out.println("子任务提交完毕"); // 获取子线程的返回值 try { System.out.println(futureTask.get()); } catch (Exception e) { e.printStackTrace(); } }; // 提交主线 fixedThreadPool.submit(runnable); } }
执行上诉示例后输出
pool-1-thread-1-main-thread
子任务提交完毕
然后程序假死。
使用@Async示例
// 程序入口 @Controller public class AsyncController { @Autowired private MainThreadService mainThreadService; @GetMapping("/") public String helloWorld() throws Exception { mainThreadService.asyncMethod(); return "Hello World"; } } // 主任务代码 @Service public class MainThreadService { @Autowired private ChildThreadService childThreadService; @Async("asyncThreadPool") public void asyncMethod() throws Exception { // 主任务开始 // TODO // 开启子任务 Future<Long> longFuture = childThreadService.asyncMethod(); // 子任务阻塞子任务 longFuture.get(); // TODO } } // 子任务示例 @Service public class ChildThreadService { @Async("asyncThreadPool") public Future<Long> asyncMethod() throws Exception { // 子任务执行 Thread.sleep(1000); // 返回异步结果 return new AsyncResult<>(10L); } }
定位
1.通过jps
和 jstack
命令定位
jstack 81173 | grep 'WAITING' -A 15
admin@wangyuhao spring-boot-student % jstack 81173 | grep 'WAITING' -A 15
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x000000076b541b38> (a java.util.concurrent.FutureTask)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at java.util.concurrent.FutureTask.awaitDone(FutureTask.java:429)
at java.util.concurrent.FutureTask.get(FutureTask.java:191)
at com.xiaolyuh.FutureBlockTest.lambda$main$1(FutureBlockTest.java:28)
at com.xiaolyuh.FutureBlockTest$$Lambda$1/885951223.run(Unknown Source)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
at java.util.concurrent.FutureTask.run$$$capture(FutureTask.java:266)
at java.util.concurrent.FutureTask.run(FutureTask.java)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
可以定位到是futureTask.get()
发生了阻塞。
2.也可以使用 Arthas定位
状态 | 场景 | 原因 |
---|---|---|
BLOCKED | 线程处于BLOCKED状态的场景 | 1.当前线程在等待一个monitor lock,比如synchronizedhuo或者Lock。 |
WAITING | 线程处于WAITING状态的场景 | 1. 调用Object对象的wait方法,但没有指定超时值。 2. 调用Thread对象的join方法,但没有指定超时值。 3. 调用LockSupport对象的park方法。 |
TIMED_WAITING | 线程处于TIMED_WAITING状态的场景 | 1. 调用Thread.sleep方法。 2. 调用Object对象的wait方法,指定超时值。 3. 调用Thread对象的join方法,指定超时值。 4. 调用LockSupport对象的parkNanos方法。 5. 调用LockSupport对象的parkUntil方法。 |
问题分析
线程池内部结构
当线程1中的任务A嵌套了任务C后,任务C被放到了阻塞队列,这时线程1就被柱塞了,必须等到任务C执行完毕。
这时如果其他线程也发生相同清空,如线程2的任务B,他的嵌套任务D也被放入阻塞队列,这是线程2也会被阻塞。
如果这类任务比较多时就会将所有线程池的线程阻塞住。最后导致线程池假死,所有异步任务无法执行。
解决办法
- futureTask.get()必须加上超时时间,这样至少不会导致程序一直假死
- 不要使用嵌套的异步任务,或者嵌套任务不要获取子任务结果,不要阻塞主任务
- 将主任务和子任务的线程池拆分成两个线程池池,不要使用同一个线程池(推荐)
思考
我们程序代码使用的@Async注解,也就是示例二的代码。使用注解默认配置,那么Spring会给所有任务分配单独线程,且线程不能重用,源码如下:
获取Executor源码
org.springframework.aop.interceptor.AsyncExecutionInterceptor#getDefaultExecutor
/** * This implementation searches for a unique {@link org.springframework.core.task.TaskExecutor} * bean in the context, or for an {@link Executor} bean named "taskExecutor" otherwise. * If neither of the two is resolvable (e.g. if no {@code BeanFactory} was configured at all), * this implementation falls back to a newly created {@link SimpleAsyncTaskExecutor} instance * for local use if no default could be found. * @see #DEFAULT_TASK_EXECUTOR_BEAN_NAME */ @Override protected Executor getDefaultExecutor(BeanFactory beanFactory) { Executor defaultExecutor = super.getDefaultExecutor(beanFactory); return (defaultExecutor != null ? defaultExecutor : new SimpleAsyncTaskExecutor()); }
获取执行任务源码
org.springframework.core.task.SimpleAsyncTaskExecutor#doExecute
/** * Template method for the actual execution of a task. * <p>The default implementation creates a new Thread and starts it. * @param task the Runnable to execute * @see #setThreadFactory * @see #createThread * @see java.lang.Thread#start() */ protected void doExecute(Runnable task) { Thread thread = (this.threadFactory != null ? this.threadFactory.newThread(task) : createThread(task)); thread.start(); }
我们可以发现默认执行@Async注解的异步线程池,内部其实就没用线程池,它会给每一个任务创建一个新的线程,线程使用过后会销毁掉,线程不会重用。
- 那它将会带来一个问题,那就是异步任务过多就会不断创建线程,最终将系统资源耗尽。
- 这也是网络上大部分文章不推荐直接使用@Async注解默认配置的原因。
我们需要思考的是,Spring的设计这为什么要这样设计,这里有这么明显的问题,难道他们不知道吗,我理解这样设计的初衷可能就是为了避免上诉我们发现的任务嵌套问题,因为每个任务单独线程执行是不会发生上诉程序假死的情况的。
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。