Статьи

Параллельное и асинхронное программирование в Java 8

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

Параллельные потоки

До Java 8 существовала большая разница между параллельным (или параллельным) кодом и последовательным кодом. Также было очень трудно отлаживать непоследовательный код. Простая установка точки останова и прохождение потока, как вы обычно делаете, удалили бы параллельный аспект, который является проблемой, если именно это является причиной ошибки.

К счастью, Java 8 дала нам потоки, величайшая вещь для разработчиков Java со времен bean-компонента. Если вы не знаете, что это такое, Stream API позволяет обрабатывать последовательности элементов в функциональном отношении. (Проверьте наше сравнение между потоками и LINQ .NET.) Одним из преимуществ потоков является то, что структура кода остается неизменной: последовательный или параллельный, он остается таким же читаемым.

Чтобы ваш код выполнялся параллельно, вы просто используете .parallelStream() вместо .stream() или (или stream .parallel() , если вы не являетесь создателем потока).

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

Вопрос скорости

Параллельный код получает преимущество в скорости благодаря использованию нескольких потоков вместо одного, который использует последовательный код. Решить, сколько потоков создать, может быть непростым вопросом, потому что большее количество потоков не всегда приводит к более быстрому коду: если вы используете слишком много потоков, производительность вашего кода может фактически снизиться.

Есть пара правил, которые скажут вам, какое количество потоков выбрать. Это зависит в основном от вида операции, которую вы хотите выполнить, и количества доступных ядер.

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

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

================================================== ====================

Going Async

Уроки из JavaScript

Редкий случай, когда разработчик Java может сказать, что они чему-то научились, глядя на JavaScript, но когда дело доходит до асинхронного программирования, JavaScript действительно сначала понял это правильно. Как фундаментально асинхронный язык, JavaScript имеет большой опыт в том, насколько болезненным он может быть при плохой реализации. Это началось с обратных вызовов и было позже заменено обещаниями. Важным преимуществом обещаний является то, что у него есть два «канала»: один для данных и один для ошибок. Обещание JavaScript может выглядеть примерно так:

1
2
3
4
5
func
.then(f1)
.catch(e1)
.then(f2)
.catch(e2);

Поэтому, когда исходная функция имеет успешный результат, вызывается f1, но если была выдана ошибка, вызывается e1. Это может вернуть его на успешную дорожку (f2) или привести к другой ошибке (e2). Вы можете перейти от трека данных к треку ошибок и обратно.

Java-версия обещаний JavaScript называется CompletableFuture .

CompletableFuture

CompletableFuture реализует интерфейс Future и CompletionStage . Future уже существовало до Java8, но само по себе оно не было очень удобным для разработчиков. Вы можете получить результат асинхронного вычисления только с помощью .get() , который блокирует остальное (большую часть времени делая асинхронную часть довольно бессмысленной), и вам необходимо реализовать каждый возможный сценарий вручную. Добавление интерфейса CompletionStage стало прорывом, благодаря которому асинхронное программирование на Java стало работоспособным.

CompletionStage — это обещание, а именно обещание, что вычисления в конечном итоге будут выполнены. Он содержит кучу методов, которые позволяют вам присоединять обратные вызовы, которые будут выполняться после этого завершения. Теперь мы можем обработать результат без блокировки.

Есть два основных метода, которые позволяют запустить асинхронную часть вашего кода: supplyAsync если вы хотите что-то сделать с результатом метода, и runAsync если вы этого не сделаете.

1
2
CompletableFuture.runAsync(() → System.out.println("Run async in completable future " + Thread.currentThread()));
CompletableFuture.supplyAsync(() → 5);

Callbacks

Теперь вы можете добавить эти обратные вызовы для обработки результата вашего supplyAsync

1
2
3
4
CompletableFuture.supplyAsync(() → 5)
.thenApply(i → i * 3)
.thenAccept(i → System.out.println(“The result is “ + i)
.thenRun(() → System.out.println("Finished."));

.thenApply аналогична функции .map для потоков: она выполняет преобразование. В приведенном выше примере он берет результат (5) и умножает его на 3. Затем он передает этот результат (15) дальше по трубе.

.thenAccept выполняет метод для результата без его преобразования. Это также не вернет результат. Здесь будет выведено «Результат 15» на консоль. Это можно сравнить с методом .foreach для потоков.

.thenRun не использует результат асинхронной операции и также ничего не возвращает, он просто ожидает вызова Runnable пока предыдущий шаг не будет завершен.

Асинхронизация вашего асинхронного

Все вышеперечисленные методы обратного вызова также имеют асинхронную версию: thenRunAsync , thenApplyAsync и т. Д. Эти версии могут работать в своем собственном потоке, и они дают вам дополнительный контроль, потому что вы можете сказать ему, какой ForkJoinPool использовать.

Если вы не используете асинхронную версию, все обратные вызовы будут выполняться в одном потоке.

Когда дела идут плохо

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

1
2
3
.exceptionally(ex → new Foo())
.thenAccept(this::bar);

Объединить и составить

Вы можете thenCompose несколько цепочек CompletableFutures с помощью метода thenCompose . Без него результат был бы вложенным CompletableFutures . Это делает thenCompose и thenApply как flatMap и map для потоков.

1
2
3
CompletableFuture.supplyAsync(() -> "Hello")
.thenCompose(s -> CompletableFuture
.supplyAsync(() -> s + "World"));

Если вы хотите объединить результат двух CompletableFutures , вам понадобится метод, который thenCombine называется thenCombine .

1
2
future.thenCombine(future2, Integer::sum)
.thenAccept(value →  System.out.println(value));

Как вы можете видеть в приведенном выше примере, результат обратного вызова в thenCombine может обрабатываться как обычный CompletableFuture со всеми вашими любимыми методами CompletionStage .

Вывод

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

Смотрите оригинальную статью здесь: Параллельное и асинхронное программирование в Java 8

Мнения, высказанные участниками Java Code Geeks, являются их собственными.