В этой статье я расскажу о том, как можно упорядочить разные потоки, чтобы они выполнялись в желаемом порядке. Конечно, существуют традиционные способы обеспечения порядка, но я хочу показать вам элегантный способ под названием CompletableFuture, который был представлен в Java 8.

1- Что такое CompletableFuture?

CompletableFuture - это новый способ выполнения потоков, который, как вы сейчас увидите, очень прост в использовании. Давайте посмотрим, как с этим можно выполнить поток. Во-первых, мы создаем класс Runnable.

public class TextDownloader implements Runnable {

    @Override
    public void run() {
        // Download text from server
        System.out.println("I am downloading the file containing text to a directory");
        try {
            Thread.sleep(2000); // for simulating download wait time
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

В TextDownloader class я моделирую операцию загрузки, для завершения которой требуется некоторое время. Конечно, мы хотим запустить операцию загрузки в отдельном потоке, как показано ниже.

CompletableFuture
        .runAsync(new TextDownloader());

Посмотрите, как это просто и красиво! Теперь добавим к нему еще одну часть и обработаем загруженный текст. Когда мы завершаем загрузку текста (в виде файла), мы сохраняем его в каком-либо месте в нашей файловой системе, чтобы обработать его позже. Вот ключевая часть, нам нужно убедиться, что этап загрузки завершен, чтобы мы могли получить к нему доступ позже для обработки. Вот класс процессора

public class TextProcessor implements Runnable {

    @Override
    public void run() {
        // Process downloaded text
        System.out.println("I am taking the text from the location and processing the text");
    }
}

И мы запускаем их следующим образом, чтобы обеспечить заказ

CompletableFuture
        .runAsync(new TextDownloader())
        .thenRunAsync(new TextProcessor());

Метод thenRunAsync гарантирует, что поток, выполняемый с помощью runAsync, завершен, а затем выполняет то, что ему передано. В приведенном выше примере мы предположили, что второй поток уже знает, где первый поток помещает загруженный файл в файловую систему. Но что, если это не так? Давайте узнаем, как мы можем справиться с этим с помощью поставщиков.

public class TextDownloader2 implements Supplier<String> {

    @Override
    public String get() {
        System.out.println("I am downloading the file containing text to a directory");
        try {
            Thread.sleep(2000); // for simulating download wait time
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "pathToDownloadedFile";
    }
}

Выше вы можете увидеть обновленную версию текстового загрузчика. Разница в том, что теперь он реализует интерфейс поставщика, метод get которого возвращает значение. Мы можем передать возвращаемое значение классу процессора, чтобы он знал, где найти загруженный файл. Обновленный класс текстового процессора теперь реализует интерфейс потребителя для использования вывода, предоставленного поставщиком.

public class TextProcessor2 implements Consumer<String> {

    @Override
    public void accept(String s) {
        // Process downloaded text
        System.out.println("I am taking the text from the location and processing the text");
        System.out.println("Path: " + s);
    }
}

И как мы можем использовать их вместе

CompletableFuture
        .supplyAsync(new TextDownloader2())
        .thenAccept(new TextProcessor2());

Вы также можете предоставить свою логику непосредственно внутри thenAccept следующим образом

CompletableFuture
        .supplyAsync(new TextDownloader2())
        .thenAccept(path -> {
            // some logic
        });

В классе CompletableFuture есть разные методы, которые вы можете использовать, например thenAcceptAsync или thenApply, но я не хочу пока делать статью длиннее.

В этом руководстве я говорил о порядке выполнения потоков. Я надеюсь, тебе это нравится.