为何CompletableFuture.get()被Thread.sleep()阻塞?队列大小影响超时概率
先看你的测试代码:
import com.google.common.collect.Queues; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.function.Supplier; public class TaskSleepTest { private static final ThreadPoolExecutor EXECUTOR = new ThreadPoolExecutor(4, 16, 1, TimeUnit.MINUTES, Queues.newLinkedBlockingQueue(10)); public static void main(String[] args) { for (int i = 0; i < 30; i++) { new Thread(() -> { for (int i1 = 0; i1 < 2000; i1++) { Supplier<Void> task = () -> { try { TimeUnit.MILLISECONDS.sleep(200); } catch (InterruptedException e) { throw new RuntimeException(e); } return null; }; testSleep(task); } }).start(); } } private static void testSleep(Supplier<Void> task) { long start = System.currentTimeMillis(); try { CompletableFuture.supplyAsync(task, EXECUTOR).whenComplete((t, throwable) -> { if (throwable != null) { System.out.println("error"); } }).get(1000, TimeUnit.MILLISECONDS); long end = System.currentTimeMillis(); System.out.println("success ----------" + Thread.currentThread().getName() + ":" + (end - start)); } catch (TimeoutException ex) { long end = System.currentTimeMillis(); System.out.println("TimeoutException ----------" + Thread.currentThread().getName() + ":" + (end - start)); } catch (Exception ex) { long end = System.currentTimeMillis(); System.out.println("Exception ----------" + Thread.currentThread().getName() + ":" + (end - start)); } } }
一、为什么CompletableFuture.get()会被Thread.sleep()阻塞?
CompletableFuture.supplyAsync(task, EXECUTOR)是把任务提交到指定线程池执行,任务里的TimeUnit.MILLISECONDS.sleep(200)是在线程池的工作线程中休眠,这属于任务执行的必要环节,任务必须等休眠结束才算完成。
而get(1000, TimeUnit.MILLISECONDS)是在调用线程(也就是main方法中启动的30个线程)中阻塞等待任务执行完成,只有当工作线程把任务执行完(包括休眠的200ms),get()才能拿到结果并解除阻塞。本质上是get()在等待任务的总执行时间,任务里的sleep直接拉长了执行时长,导致get()被阻塞。
二、LinkedBlockingQueue队列大小为什么会影响TimeoutException的出现概率?
这要结合ThreadPoolExecutor的任务调度逻辑来看,你的线程池配置是:核心线程数4,最大线程数16,存活时间1分钟,队列是LinkedBlockingQueue。线程池处理任务的优先级顺序是:
- 优先用核心线程执行任务,直到核心线程全部被占用;
- 核心线程满了之后,新任务会被放入队列等待;
- 队列满了之后,才会创建非核心线程,直到达到最大线程数16;
- 队列和最大线程数都满了,才会触发拒绝策略。
当队列大小小于16(比如原配置的10):
核心线程4个被占满后,任务会先放进队列,队列最多放10个,此时核心线程+队列任务数=14,再来新任务时队列已经满了,线程池会立刻创建非核心线程,直到达到最大16个工作线程。
16个工作线程可以并行处理任务,每个任务耗时200ms,每秒能处理约80个任务,足以应对你30个线程的提交压力,任务几乎不需要排队就能被执行,总耗时在200-300ms内,远小于get()设置的1000ms超时时间,所以不会抛出TimeoutException。
当队列大小调整为30:
核心线程4个满了之后,新任务会先放进队列,队列可以容纳30个任务,此时核心线程+队列任务数=34,远大于最大线程数16,线程池不会创建任何非核心线程,所有额外任务都积压在队列里排队。
大量任务在队列中等待执行的时间,加上任务本身的200ms执行时间,很容易超过get()设置的1000ms超时阈值,因此会频繁出现TimeoutException。
内容的提问来源于stack exchange,提问作者jason wu

