javacompletable-future

Why is thenAccept not working in the loop?


Playing around with CompletableFutures and I coincided these outputs that I am not able to make sense of. I will share two snippets which should behave same in my perception. But they are not.

So the first program goes like:

public static void main(String... args)  {
        for (int i = 0; i < 5; i++) {
            CompletableFuture.supplyAsync(() -> Thread.currentThread().getName())
                    .thenApply(threadName -> {
                        delay();
                        List<String> parsedName = Arrays.stream(threadName.split("-")).toList();
                        return parsedName.get(parsedName.size() - 1);
                    })
                    .thenAcceptAsync(System.out::println);
        }
    }

    static void delay() {
        try {
            Thread.sleep(1000);
        } catch(InterruptedException ignored) {
            System.out.println("Interrupted");
        }
    }

The output for this program is: 1

Where as the second program writes the completablefutures without the for loop, again 5 times:

   public static void main(String... args)  {
        CompletableFuture.supplyAsync(() -> Thread.currentThread().getName())
                .thenApply(threadName -> {
                    delay();
                    List<String> parsedName = Arrays.stream(threadName.split("-")).toList();
                    return parsedName.get(parsedName.size() - 1);
                })
                .thenAcceptAsync(System.out::println);
        CompletableFuture.supplyAsync(() -> Thread.currentThread().getName())
                .thenApply(threadName -> {
                    delay();
                    List<String> parsedName = Arrays.stream(threadName.split("-")).toList();
                    return parsedName.get(parsedName.size() - 1);
                })
                .thenAcceptAsync(System.out::println);
        CompletableFuture.supplyAsync(() -> Thread.currentThread().getName())
                .thenApply(threadName -> {
                    delay();
                    List<String> parsedName = Arrays.stream(threadName.split("-")).toList();
                    return parsedName.get(parsedName.size() - 1);
                })
                .thenAcceptAsync(System.out::println);
        CompletableFuture.supplyAsync(() -> Thread.currentThread().getName())
                .thenApply(threadName -> {
                    delay();
                    List<String> parsedName = Arrays.stream(threadName.split("-")).toList();
                    return parsedName.get(parsedName.size() - 1);
                })
                .thenAcceptAsync(System.out::println);
        CompletableFuture.supplyAsync(() -> Thread.currentThread().getName())
                .thenApply(threadName -> {
                    delay();
                    List<String> parsedName = Arrays.stream(threadName.split("-")).toList();
                    return parsedName.get(parsedName.size() - 1);
                })
                .thenAcceptAsync(System.out::println);
    }

    static void delay() {
        try {
            Thread.sleep(1000);
        } catch(InterruptedException ignored) {
            System.out.println("Interrupted");
        }
    }

The output of this program is consistently: 1 1 1 1 1

I do not really understand why the first program with for loop does not print out anything even though two programs look identical semantically(in my eyes). Thank you, looking forward to replies.


Solution

  • Neither of these implementations wait for all of the futures to complete. Either one could potentially output anything from zero to five numbers. I get different results when I run them on my machine, and always see five numbers printed from both versions.

    To guarantee this, you should collect the CompletableFuture instances into an array and then ensure they have completed before main exits:

    public static void main(String... args) {
        var futures = new CompletableFuture[5];
        for (int i = 0; i < 5; i++) {
            futures[i] = CompletableFuture.supplyAsync(() -> Thread.currentThread().getName())
                    .thenApply(threadName -> {
                        delay();
                        List<String> parsedName = Arrays.stream(threadName.split("-")).toList();
                        return parsedName.get(parsedName.size() - 1);
                    })
                    .thenAcceptAsync(System.out::println);
        }
        CompletableFuture.allOf(futures).join();
    }
    

    The non-looping equivalent would be very similar:

    public static void main(String... args) {
        var futures = new CompletableFuture[5];
        futures[0] = CompletableFuture.supplyAsync(() -> Thread.currentThread().getName())
                .thenApply(threadName -> {
                    delay();
                    List<String> parsedName = Arrays.stream(threadName.split("-")).toList();
                    return parsedName.get(parsedName.size() - 1);
                })
                .thenAcceptAsync(System.out::println);
        futures[1] = CompletableFuture.supplyAsync(() -> Thread.currentThread().getName())
                .thenApply(threadName -> {
                    delay();
                    List<String> parsedName = Arrays.stream(threadName.split("-")).toList();
                    return parsedName.get(parsedName.size() - 1);
                })
                .thenAcceptAsync(System.out::println);
        futures[2] = CompletableFuture.supplyAsync(() -> Thread.currentThread().getName())
                .thenApply(threadName -> {
                    delay();
                    List<String> parsedName = Arrays.stream(threadName.split("-")).toList();
                    return parsedName.get(parsedName.size() - 1);
                })
                .thenAcceptAsync(System.out::println);
        futures[3] = CompletableFuture.supplyAsync(() -> Thread.currentThread().getName())
                .thenApply(threadName -> {
                    delay();
                    List<String> parsedName = Arrays.stream(threadName.split("-")).toList();
                    return parsedName.get(parsedName.size() - 1);
                })
                .thenAcceptAsync(System.out::println);
        futures[4] = CompletableFuture.supplyAsync(() -> Thread.currentThread().getName())
                .thenApply(threadName -> {
                    delay();
                    List<String> parsedName = Arrays.stream(threadName.split("-")).toList();
                    return parsedName.get(parsedName.size() - 1);
                })
                .thenAcceptAsync(System.out::println);
        CompletableFuture.allOf(futures).join();
    }