CompletableFuture使用不当导致主线程执行回调

1,653 阅读3分钟

前几天看博客说默认情况下一个CompletableFuture对象对应的异步任务和在同一个CompletableFuture对象上添加的回调事件都是使用ForkJoinPool.commonPool()中的同一个线程来执行的且由于是栈结构,在同一个CompletableFuture对象上行为注册的顺序与行为执行的顺序是相反的。

代码验证

public class CompletableTest {
    private static final Integer AVALIABLE_PROCESSORS = Runtime.getRuntime().availableProcessors();
    private static final ThreadPoolExecutor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(AVALIABLE_PROCESSORS,
            AVALIABLE_PROCESSORS * 2, 1 , TimeUnit.MINUTES, new LinkedBlockingQueue<>(5), new ThreadPoolExecutor.AbortPolicy());
    public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
        CompletableFuture future = CompletableFuture.supplyAsync(() -> {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("0" + Thread.currentThread().getName());
            return "1";
        },THREAD_POOL_EXECUTOR);

        CompletableFuture future1 = future.thenApply(new Function<String, Integer>() {

            @Nullable
            @Override
            public Integer apply(@Nullable String o) {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("1" + Thread.currentThread().getName());
                return Integer.valueOf(o);
            }
        });

        CompletableFuture<Integer> future2 = future.thenApply(new Function<String, Integer>() {
            @Nullable
            @Override
            public Integer apply(@Nullable String o) {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("2" + Thread.currentThread().getName());
                return Integer.valueOf(o);
            }
        });

        CompletableFuture<Integer> future3 = future.thenApply(new Function<String, Integer>() {
            @Nullable
            @Override
            public Integer apply(@Nullable String o) {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("3" + Thread.currentThread().getName());
                return Integer.valueOf(o);
            }
        });
        CompletableFuture<Integer> future4 = future.thenApply(new Function<String, Integer>() {
            @Nullable
            @Override
            public Integer apply(@Nullable String o) {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("4" + Thread.currentThread().getName());
                return Integer.valueOf(o);
            }
        });

        CompletableFuture<String> future5 = future3.thenApply(new Function<Integer, String>() {
            @Nullable
            @Override
            public String apply(@Nullable Integer o) {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("5" + Thread.currentThread().getName());
                return o.toString();
            }
        });
        Long now = System.currentTimeMillis();
        System.out.println(future.get());
        System.out.println(System.currentTimeMillis() - now);
        System.out.println(future1.get());
        System.out.println(System.currentTimeMillis() - now);
        System.out.println(future2.get());
        System.out.println(System.currentTimeMillis() - now);
        System.out.println(future3.get());
        System.out.println(System.currentTimeMillis() - now);
        System.out.println(future4.get());
        System.out.println(System.currentTimeMillis() - now);
        System.out.println(future5.get());
        System.out.println(System.currentTimeMillis() - now);
    }
}

测试结果

0pool-1-thread-1
4pool-1-thread-1
3main
2pool-1-thread-1
5main
1pool-1-thread-1

测试结果怎么会用到了主线程main,明显不符合上述执行异步线程和回调都由同一个线程来处理的说法。

结论

1.如果CompletableFuture对象对应的异步任务再主线程中进行get() 操作,则其对应的多个回调中某些回调将由主线程进行处理,与CompletableFuture对象对应的异步任务的线程不是同一个。

如果在其他线程中调用CompletableFuture对象对应的异步任务的get() 方法,则会将此线程作为可执行其回调的线程之一。

实验:

通过主线程/其他线程在异步任务执行完后调用以下三个方法做对比:

public T getNow(T valueIfAbsent) {
    Object r;
    return ((r = result) == null) ? valueIfAbsent : reportJoin(r);
}


public T get() throws InterruptedException, ExecutionException {
    Object r;
    return reportGet((r = result) == null ? waitingGet(true) : r);
}


public void obtrudeValue(T value) {
    result = (value == null) ?NIL: value;
    postComplete();
}

排查与被volatile修饰的result属性无关,目前测试结果可能和postComplete方法有关;

解释:

CompletableFuture对象对应的异步任务执行后,则会调用postComplete中调用uniApply执行回调,但是如果主线程调用get()等带有postComplete的方法并且异步任务执行执行已经完成,则会由主线程参与调用回调。

2.如果CompletableFuture对象对应的异步任务是立即完成的(将上述代码sleep(2000)去掉),那么其回调将在主线程中进行处理,也会导致主线程阻塞。

猜测的原因:异步任务执行结束时还未把回调放入栈中,导致回调只能被主线程所执行。

导致的后果:如果由主线程进行处理的某些回调比较耗时,那么会造成主线程阻塞,所以尽量使用thenApplyAsync等方法。

如果如果CompletableFuture对象对应的异步任务再主线程中进行get() 操作在主线程进行另外操作之前,则导致主线程被回调占用,导致主线程正常流程无法运行,使用thenApplyAsync等方法不会出现此问题。