[Перевод] Раскрытие возможностей асинхронного программирования в Core Java

image

Введение


В сфере разработки современного программного обеспечения успех напрямую зависит от отзывчивости и масштабируемости. Асинхронное программирование в Core Java помогает разработчикам мощный арсенал для решения этих задач. В этом подробном посте мы погрузимся в мир асинхронного программирования в Core Java, исследуем соответствующие концепции, техники и практику применения на наглядных примерах кода.

Понимание асинхронного программирования в Core Java


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

Ниже показано, как использовать асинхронные операции файлового ввода-вывода в Core Java для чтения из файлов и записи в них без блокировки вызывающего потока. Так повышается отзывчивость и масштабируемость приложения.

import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.concurrent.Future;

public class AsyncFileIOExample {

    public static void main(String[] args) {
        // Укажем путь к файлу
        String filePath = "data.txt";

        // Создаем буфер ByteBuffer для хранения данных, считанных из файла или записанных в него
        ByteBuffer buffer = ByteBuffer.allocate(1024);

        try {
            // Асинхронное открытие файла для чтения и записи         AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(
                    Paths.get(filePath),
                    StandardOpenOption.READ,
                    StandardOpenOption.WRITE
            );

            // Асинхронное чтение данных из файла
            Future readFuture = fileChannel.read(buffer, 0);
            while (!readFuture.isDone()) {
                // Выполняем другие задачи в ожидании завершения операции чтения
                System.out.println("Reading file asynchronously...");
            }

            // Отображение считанных данных
            buffer.flip();
            System.out.println("Data read from file: ");
            while (buffer.hasRemaining()) {
                System.out.print((char) buffer.get());
            }

            // Асинхронная запись данных в файл
            String newData = "Hello, World!";
            buffer.clear();
            buffer.put(newData.getBytes());
            buffer.flip();
            Future writeFuture = fileChannel.write(buffer, 0);
            while (!writeFuture.isDone()) {
                // Выполняем другие задачи в ожидании завершения операции записи
                System.out.println("Writing to file asynchronously...");
            }

            // Закрываем канал передачи файлов
            fileChannel.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


  1. Начнем с импорта необходимых классов из пакета java.nio, включая AsynchronousFileChannel для асинхронных операций ввода-вывода файлов.
  2. Мы указываем путь к файлу, с которым хотим выполнять асинхронные операции (data.txt), и создаем ByteBuffer для хранения данных, прочитанных из файла или записанных в него данных.
  3. Внутри метода main мы открываем файл асинхронно как для чтения, так и для записи с помощью метода AsynchronousFileChannel.open ().
  4. Мы инициируем операцию асинхронного чтения файла с помощью метода read () класса AsynchronousFileChannel. Этот метод возвращает объект Future, представляющий результат операции асинхронного чтения.
  5. В ожидании завершения операции чтения (т. е. пока объект Future еще не завершен) мы можем выполнять другие задачи, не блокируя вызывающий поток.
  6. После завершения операции чтения мы переворачиваем буфер ByteBuffer, чтобы подготовить его к чтению и отобразить данные, считанные из файла.
  7. Далее мы выполняем асинхронную операцию записи в файл с помощью метода write () класса AsynchronousFileChannel. И снова этот метод возвращает объект Future, представляющий результат асинхронной операции записи.
  8. Ожидая завершения операции записи, мы можем выполнять другие задачи, не блокируя вызывающий поток.
  9. Наконец, мы закрываем файловый канал, чтобы освободить системные ресурсы.
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.concurrent.CompletableFuture;

public class AsyncHttpRequestExample {

    public static void main(String[] args) {
        // Определяем URL-адресов для HTTP-запросов
        String[] urls = {
            "https://jsonplaceholder.typicode.com/posts/1",
            "https://jsonplaceholder.typicode.com/posts/2",
            "https://jsonplaceholder.typicode.com/posts/3"
        };

        // Создаем экземпляр HttpClient
        HttpClient httpClient = HttpClient.newHttpClient();

        // Выполняем асинхронных запросов HTTP
        CompletableFuture allRequests = CompletableFuture.allOf(
            CompletableFuture.runAsync(() -> sendHttpRequest(httpClient, urls[0])),
            CompletableFuture.runAsync(() -> sendHttpRequest(httpClient, urls[1])),
            CompletableFuture.runAsync(() -> sendHttpRequest(httpClient, urls[2]))
        );

        // Ожидание завершения всех запросов
        allRequests.join();
    }

    private static void sendHttpRequest(HttpClient httpClient, String url) {
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(url))
            .build();

        // Асинхронная отправка HTTP-запроса
        CompletableFuture> responseFuture =
            httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString());

        // Асинхронная обработка HTTP-ответов
        responseFuture.thenAccept(response -> {
            System.out.println("Response from " + url + ":");
            System.out.println(response.body());
        }).exceptionally(ex -> {
            System.err.println("Failed to send HTTP request: " + ex.getMessage());
            return null;
        });
    }
}


  1. Начнем с импорта необходимых классов из пакета java.net, включая HttpClient, HttpRequest, HttpResponse и CompletableFuture.
  2. Внутри метода main мы определяем массив URL-адресов для HTTP-запросов, которые мы хотим выполнять асинхронно.
  3. Создаем экземпляр HttpClient с помощью HttpClient.newHttpClient ().
  4. Мы используем API CompletableFuture для одновременного выполнения нескольких HTTP-запросов. Каждый запрос отправляется асинхронно с помощью CompletableFuture.runAsync (), который выполняет метод sendHttpRequest () в отдельном потоке.
  5. Метод sendHttpRequest () создает объект HttpRequest для указанного URL и отправляет HTTP-запрос асинхронно с помощью HttpClient.sendAsync ().
  6. Мы предусматриваем этап завершения в каждом responseFuture с помощью метода thenAccept (), который асинхронно обрабатывает HTTP-ответ, когда он становится доступным. Мы выводим тело ответа в консоль.
  7. Мы обрабатываем любые исключения, возникающие во время HTTP-запроса, с помощью метода exceptionally (), который позволяет нам изящно обрабатывать ошибки.
  8. Наконец, мы ждем завершения всех запросов, вызывая метод join () на CompletableFuture, возвращаемом CompletableFuture.allOf (). Это гарантирует, что главный поток дождется завершения всех асинхронных задач перед выходом.


Освоение конкурентности с помощью исполнителей


Конкурентность лежит в основе асинхронного программирования. При помощи конкурентности обеспечивается одновременное выполнение нескольких задач и удаётся эффективно использовать системные ресурсы. В этом разделе мы разберём фреймворк Java Executor — мощный инструмент для управления выполнением потоков. Мы изучим различные типы исполнителей, стратегии объединения потоков и расширенные возможности для тонкого контроля над параллелизмом.

В этом примере мы покажем, как использовать ThreadPoolExecutor в Java для одновременного выполнения нескольких задач. ThreadPoolExecutor обеспечивает гибкий и эффективный механизм управления пулом потоков, позволяя нам контролировать количество потоков и эффективно управлять выполнением задач.

import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;

public class ThreadPoolExecutorExample {

    public static void main(String[] args) {
        // Создаем ThreadPoolExecutor с пулом потоков фиксированного размера
        ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(3);

        // Передаем задания исполнителю
        for (int i = 1; i <= 5; i++) {
            Task task = new Task("Task " + i);
            System.out.println("Submitting " + task.getName());
            executor.execute(task);
        }

        // Завершаем работу исполнителя после выполнения всех задач
        executor.shutdown();
    }

    static class Task implements Runnable {
        private final String name;

        public Task(String name) {
            this.name = name;
        }

        public String getName() {
            return name;
        }

        @Override
        public void run() {
            System.out.println("Executing " + name);
            try {
                Thread.sleep(1000); // Моделирование времени выполнения задачи
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            System.out.println(name + " completed");
        }
    }
}


  1. Импортируем необходимые классы из пакета java.util.concurrent, включая Executors и ThreadPoolExecutor.
  2. Внутри метода main мы создаем ThreadPoolExecutor с пулом потоков фиксированного размера с помощью Executors.newFixedThreadPool (3). При этом создается пул потоков с тремя потоками.
  3. Мы отправляем пять задач исполнителю с помощью метода execute (). Каждая задача представлена экземпляром класса Task, который реализует интерфейс Runnable.
  4. Класс Task определяет метод run (), который имитирует выполнение задачи, печатая сообщения на консоли и засыпая на 1 секунду с помощью Thread.sleep (1000).
  5. Исполнитель управляет одновременным выполнением заданий, используя доступные потоки в пуле потоков. Задания выполняются параллельно, и исполнитель автоматически назначает потоки для выполнения представленных заданий.
  6. После отправки всех заданий мы вызываем метод shutdown () на исполнителе, чтобы изящно закрыть пул потоков после завершения всех заданий. Это гарантирует, что исполнитель перестанет принимать новые задачи и позволит существующим задачам завершить выполнение.


Навигация по асинхронным операциям ввода-вывода с помощью Java NIO


Асинхронные операции ввода-вывода крайне важны при создании отзывчивых и масштабируемых приложений, особенно в сценариях, связанных с сетевым взаимодействием и файловым вводом-выводом. В этой главе мы изучим пакет Java New I/O (NIO) и его поддержку неблокирующих операций ввода/вывода. На практических примерах мы узнаем, как использовать каналы, селекторы и асинхронные каналы для эффективной обработки операций ввода-вывода.

На этом примере мы продемонстрируем, как выполнять неблокирующие операции ввода-вывода файлов с помощью AsynchronousFileChannel в Java. AsynchronousFileChannel позволяет нам читать из файлов и писать в них асинхронно, не блокируя вызывающий поток. Это особенно полезно при работе с большими файлами или в тех случаях, когда скорость отклика очень важна.

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

public class AsynchronousFileChannelExample {

    public static void main(String[] args) {
        try {
            // Асинхронное открытие файла для записи
            AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(
                    Paths.get("example.txt"), StandardOpenOption.WRITE);

            // Асинхронная запись данных в файл
            ByteBuffer buffer = ByteBuffer.wrap("Hello, World!".getBytes());
            Future future = fileChannel.write(buffer, 0);
            future.get(); // Ожидаем, пока операция записи завершится

            System.out.println("Data has been written to the file asynchronously.");

            // Закрываем файловый канал
            fileChannel.close();
        } catch (IOException | InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}


  1. Мы импортируем необходимые классы из пакета java.nio, включая AsynchronousFileChannel, ByteBuffer, Paths и StandardOpenOption…
  2. Внутри метода main мы асинхронно открываем файл для записи с помощью метода AsynchronousFileChannel.open (). Мы указываем путь к файлу («example.txt») и опцию WRITE, чтобы указать, что мы хотим записывать в файл.
  3. Создаем ByteBuffer, содержащий данные, которые мы хотим записать в файл («Hello, World!»).
  4. Мы инициируем операцию записи асинхронно, вызывая метод write () на файловом канале. Этот метод возвращает объект Future, представляющий результат операции записи.
  5. Мы вызываем метод get () на объекте Future, чтобы дождаться завершения операции записи. Этот метод блокируется до тех пор, пока результат операции не будет доступен.
  6. После завершения операции записи мы выводим сообщение о том, что данные были записаны в файл асинхронно.
  7. Наконец, мы закрываем файловый канал, чтобы освободить системные ресурсы.


Наилучшие практики и оптимизация производительности


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

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

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

public class CompletableFutureExample {

    public static void main(String[] args) {
        // Моделирование длительного вычисления
        CompletableFuture future = CompletableFuture.supplyAsync(() -> {
            // Выполнение вычисления, которое занимает время
            simulateLongRunningTask();
            return 42;
        });

        // Выполняйте действие по завершении вычислений
        future.thenAccept(result -> {
            System.out.println("Result: " + result);
        });

        // Выполняйте другую работу во время выполнения вычислений
        doOtherWork();

        // Дождитесь завершения вычислений (блокирующая операция)
        try {
            future.get();
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }

    private static void simulateLongRunningTask() {
        // Моделирование длительного вычисления
        try {
            Thread.sleep(2000); // Моделирование 2 секунд вычислений
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private static void doOtherWork() {
        // Имитация другой работы, выполняемой во время вычислений
        System.out.println("Doing other work...");
    }
}


  1. Импортируем класс CompletableFuture и другие необходимые классы для конкурентности.
  2. Внутри метода main мы создаем CompletableFuture с помощью метода supplyAsync (). Этот метод позволяет нам асинхронно выполнять задачу, которая возвращает результат.
  3. В функции поставщика, передаваемой в supplyAsync (), мы имитируем длительное вычисление с помощью метода simulateLongRunningTask (). Этот метод усыпляет текущий поток на 2 секунды, чтобы смоделировать вычисление, которое занимает время.
  4. Мы подключаем к CompletableFuture метод thenAccept (), чтобы выполнить действие по завершении вычислений. Это позволяет нам вывести результат вычислений.
  5. Мы имитируем выполнение другой работы, вызывая метод doOtherWork (). Он представляет собой другие задачи, которые могут выполняться параллельно, пока идет длительное вычисление.
  6. Наконец, мы ждем завершения работы CompletableFuture с помощью метода get (). Это блокирующая операция, но мы минимизировали ее влияние, выполняя параллельно другую работу.


Приложения и примеры реального использования


В последнем разделе мы покажем реальные приложения и примеры использования, в которых асинхронное программирование проявляется наиболее ярко. От создания высокопроизводительных веб-серверов и масштабируемых микросервисов до решения задач одновременной обработки данных — мы рассмотрим, как асинхронное программирование в Core Java превращает теоретические концепции в практические решения.

  1. Высокопроизводительные веб-серверы: Асинхронное программирование играет важную роль в создании высокопроизводительных веб-серверов, способных эффективно обрабатывать большое количество одновременных соединений. Используя асинхронные операции ввода-вывода и неблокирующие событийно-ориентированные архитектуры, разработчики могут создавать веб-серверы, способные обслуживать несколько клиентов одновременно без необходимости создания отдельного потока для каждого соединения. В результате повышается масштабируемость и снижается потребление ресурсов.
  2. Масштабируемые микросервисы: Архитектура микросервисов предполагает разработку небольших, слабосвязанных сервисов, которые можно независимо развертывать и масштабировать. В микросервисах широко используются шаблоны асинхронного взаимодействия, такие как очереди сообщений и событийно-ориентированные архитектуры, которые позволяют обеспечить бесперебойную связь между сервисами, сохраняя при этом быстроту реакции и масштабируемость. Асинхронное программирование позволяет микросервисам обрабатывать асинхронные запросы, выполнять фоновые задачи и реагировать на события неблокирующим образом.
  3. Задачи одновременной обработки данных: Многие приложения предполагают одновременную обработку больших объемов данных, например, аналитика в реальном времени, пакетная и потоковая обработка. Асинхронное программирование позволяет разработчикам эффективно распараллеливать задачи обработки данных, используя многоядерные процессоры и распределенные вычислительные среды. Разбивая задачи на более мелкие блоки и выполняя их асинхронно, приложения могут добиться большей пропускной способности, снижения задержек и повышения общей производительности.
  4. Событийно-ориентированные приложения: Событийно-ориентированное программирование широко распространено в различных областях, включая пользовательские интерфейсы, сетевое программирование и приложения IoT (Internet of Things). Асинхронная обработка событий позволяет приложениям реагировать на внешние события, такие как взаимодействие с пользователем, данные датчиков или сетевые события, не блокируя основной поток выполнения. Это позволяет приложениям оставаться отзывчивыми и одновременно обрабатывать множество событий, повышая удобство работы пользователей и надежность системы.
  5. Связь в реальном времени: Асинхронное программирование необходимо для реализации протоколов связи в реальном времени, таких как WebSocket, MQTT и HTTP/2. Эти протоколы требуют эффективной обработки двунаправленной связи и поддержки долговременных соединений. Асинхронные операции ввода-вывода позволяют приложениям асинхронно обрабатывать входящие сообщения, параллельно обрабатывать их и отправлять ответы без блокирования канала связи, что облегчает взаимодействие между клиентами и серверами в реальном времени.


Изучив эти реальные приложения и примеры использования, разработчики смогут получить представление о практической пользе и применении асинхронного программирования в Core Java. Будь то создание высокопроизводительных веб-серверов, масштабируемых микросервисов или систем связи в реальном времени, владение методами асинхронного программирования необходимо для создания надежных и отзывчивых приложений в современном динамичном программном ландшафте.

Заключение


Асинхронное программирование в Core Java открывает мир возможностей для создания отзывчивых, масштабируемых и эффективных программных приложений. Освоив концепции, техники и лучшие практики, описанные в этом блоге, разработчики смогут раскрыть весь потенциал асинхронного программирования и отправиться в путь к созданию следующего поколения Java-приложений.

P.S. Обращаем ваше внимание на то, что у нас на сайте проходит распродажа.

© Habrahabr.ru