Одновременное исполнение на платформе JVM
Основы одновременного исполнения в Java 8
Средства Java 8 существенно упрощают программирование одновременного исполнения
Серия контента:
Этот контент является частью # из серии # статей: Одновременное исполнение на платформе JVM
Этот контент является частью серии:Одновременное исполнение на платформе JVM
Следите за выходом новых статей этой серии.
Ряд крупных улучшений в долгожданном релизе Java 8 имеют отношение к одновременному исполнению, в том числе добавленные классы в иерархии java.util.concurrent
и новая мощная параллельная опция streams (потоки). Потоки предназначены для использования совместно с лямбда-выражениями. Это дополнение Java 8, которое также упрощает многие другие аспекты повседневного программирования (в сопутствующей статье по языковым расширениям в Java 8 изложена вводная информация по лямбда-выражениям и по соответствующим изменениям в interface
).
В этой статье я сначала показываю, как новый класс CompletableFuture
упрощает координацию асинхронных операций. Затем я показываю, как использовать параллельные потоки — мощное средство для поддержки одновременного исполнения в Java 8 — для параллельного исполнения операций с наборами значений. И, наконец, я показываю, как работают новые возможности Java 8. В частности, я произвожу сравнение с программным кодом из первой статьи данного цикла (в разделе Ресурсы приведена ссылка на полный учебный код для этой статьи).
Снова о Future
В первой статье данного цикла была дана краткая вводная информация по Future
в Java и в Scala. Java-версия future (в предварительном релизе Java 8) является слабой, она поддерживает лишь два типа использования: вы можете либо проверять, не завершился ли future-объект, либо ждать завершения future-объекта. Версия Scala является гораздо более гибкой: вы можете выполнить обратные вызовы, когда future-объект завершается, а аварийные завершения обрабатывать в форме Throwable
.
В релизе Java 8 добавлен класс CompletableFuture<T>
, который реализует новый интерфейс
CompletionStage<T>
и расширяет интерфейс Future<T>
(все классы и интерфейсы для поддержки одновременного исполнения, рассматриваемые в этом разделе, находятся в пакете java.util.concurrent
). CompletionStage
представляет этап или шаг в потенциально асинхронном вычислении. Этот интерфейс определяет множество различных способов для связывания экземпляров CompletionStage
в цепочку с другими экземплярами или с кодом, например, с методами, вызов которых осуществляется при завершении (в общей сложности 59 методов, для сравнения — у интерфейса Future
этот показатель составляет 5 методов).
В листинге 1 показан класс ChunkDistanceChecker
, основанный на коде сравнения расстояния редактирования из первой статьи.
Листинг 1. Класс ChunkDistanceChecker
public class ChunkDistanceChecker { private final String[] knownWords; public ChunkDistanceChecker(String[] knowns) { knownWords = knowns; } /** * Построение списка проверяющих элементов, охватывающего весь список слов. * * @param words * @param block * @return checkers */ public static List<ChunkDistanceChecker> buildCheckers(String[] words, int block) { List<ChunkDistanceChecker> checkers = new ArrayList<>(); for (int base = 0; base < words.length; base += block) { int length = Math.min(block, words.length - base); checkers.add(new ChunkDistanceChecker(Arrays.copyOfRange(words, base, base + length))); } return checkers; } ... /** * Нахождение наилучшего расстояния от целевого слова до любого известного слова. * * @param target * @return best */ public DistancePair bestDistance(String target) { int[] v0 = new int[target.length() + 1]; int[] v1 = new int[target.length() + 1]; int bestIndex = -1; int bestDistance = Integer.MAX_VALUE; boolean single = false; for (int i = 0; i < knownWords.length; i++) { int distance = editDistance(target, knownWords[i], v0, v1); if (bestDistance > distance) { bestDistance = distance; bestIndex = i; single = true; } else if (bestDistance == distance) { single = false; } } return single ? new DistancePair(bestDistance, knownWords[bestIndex]) : new DistancePair(bestDistance); } }
Каждый экземпляр класса ChunkDistanceChecker
сравнивает целевое слово с массивом известных слов с целью нахождения наилучшего соответствия. Статический метод buildCheckers()
создает список List<ChunkDistanceChecker>
на основе всего массива известных слов и требуемого размера фрагмента. Данный класс ChunkDistanceChecker
служит основой для нескольких описываемых в этой статье одновременно исполняемых реализаций класса для поиска по наилучшему соответствию. Первым из них является класс CompletableFutureDistance0
в листинге 2.
Листинг 2. Вычисление расстояния редактирования с помощью CompletableFuture
public class CompletableFutureDistance0 extends TimingTestBase { private final List<ChunkDistanceChecker> chunkCheckers; private final int blockSize; public CompletableFutureDistance0(String[] words, int block) { blockSize = block; chunkCheckers = ChunkDistanceChecker.buildCheckers(words, block); } ... public DistancePair bestMatch(String target) { List<CompletableFuture<DistancePair>> futures = new ArrayList<>(); for (ChunkDistanceChecker checker: chunkCheckers) { CompletableFuture<DistancePair> future = CompletableFuture.supplyAsync(() -> checker.bestDistance(target)); futures.add(future); } DistancePair best = DistancePair.worstMatch(); for (CompletableFuture<DistancePair> future: futures) { best = DistancePair.best(best, future.join()); } return best; } }
В листинге 2 класс CompletableFutureDistance0
демонстрирует один из способов использования CompletableFuture
для одновременных вычислений. Метод supplyAsync()
принимает экземпляр Supplier<T>
(функциональный интерфейс с методом, возвращающий значение типа T
) и возвращает экземпляр CompletableFuture<T>
, а также помещает Supplier
в очередь для исполнения в асинхронном режиме. В первом цикле for
я передаю лямбда-выражение в метод supplyAsync()
с целью создания списка future-объектов, соответствующего массиву ChunkDistanceChecker
. Второй цикл for
ждет завершения каждого future-объекта (хотя большинство из них завершается до того, как этот цикл добирается до них, поскольку они исполняются асинхронно) и аккумулирует наилучшее соответствие из всех результатов.
Построение CompletableFuture
В первой статье данного цикла было показано, что Future
-объекты в Scala позволяют присоединять обработчики завершения и сочетать future-объекты различными способами. В языке Java 8 подобную гибкость обеспечивает класс CompletableFuture
. В этом разделе вы изучите некоторые способы использования этих возможностей в контексте кода для проверки расстояния редактирования.
В листинге 3 показана иная версия метода bestMatch()
, отличающаяся от показанной в листинге 2. Эта версия использует обработчик завершения с классом CompletableFuture
вместе с несколькими более старыми классами для одновременного исполнения.
Листинг 3. Класс CompletableFuture
с обработчиком завершения
public DistancePair bestMatch(String target) { AtomicReference<DistancePair> best = new AtomicReference<>(DistancePair.worstMatch()); CountDownLatch latch = new CountDownLatch(chunkCheckers.size()); for (ChunkDistanceChecker checker: chunkCheckers) { CompletableFuture.supplyAsync(() -> checker.bestDistance(target)) .thenAccept(result -> { best.accumulateAndGet(result, DistancePair::best); latch.countDown(); }); } try { latch.await(); } catch (InterruptedException e) { throw new RuntimeException("Interrupted during calculations", e); } return best.get(); }
В листинге 3 защелка (latch) CountDownLatch
инициализируется по количеству future-объектов, созданных в программном коде. При создании каждого future-объекта я присоединяю к нему обработчик (в форме лямбда-экземпляра функционального интерфейса java.util.function.Consumer<T>
) с помощью метода CompletableFuture.thenAccept()
. Этот обработчик, который исполняется в случае нормального завершения future-объекта, использует метод AtomicReference.accumulateAndGet()
(добавленный в релизе Java 8) для обновления найденного наилучшего значения, а затем декрементно уменьшает защелку. В это время основной поток исполнения вводит блок try-catch
и ждет освобождения защелки. После завершения всех future-объектов основной поток продолжает исполнение, возвращая в итоге найденное наилучшее значение.
В листинге 4 показана еще одна разновидность метода bestMatch()
из листинга 2.
Листинг 4. Объединение нескольких CompletableFuture
public DistancePair bestMatch(String target) { CompletableFuture<DistancePair> last = CompletableFuture.supplyAsync(bestDistanceLambda(0, target)); for (int i = 1; i < chunkCheckers.size(); i++) { last = CompletableFuture.supplyAsync(bestDistanceLambda(i, target)) .thenCombine(last, DistancePair::best); } return last.join(); } private Supplier<DistancePair> bestDistanceLambda(int i, String target) { return () -> chunkCheckers.get(i).bestDistance(target); }
В этом коде метод CompletableFuture.thenCombine ()
используется для слияния двух future-объектов посредством применения функции вида java.util.function.BiFunction
(в данном случае метода DistancePair.best()
) к двум результатам и возвращения future-объекта с результатом этой функции.
В листинге 4 представлена наиболее лаконичная и, возможно, самая "чистая" версия этого кода, однако у нее есть и недостаток: она создает дополнительный уровень объектов CompletableFuture
для представления комбинации каждой "фрагментной" операции с предшествующими операциями. В исходном релизе Java 8 это может порождать исключение StackOverflowException
, которое терялось внутри кода, в результате чего заключительный future-объект никогда не завершался. В настоящее время проводится работа над этой ошибкой; ошибка должна быть устранена в ближайшем релизе.
Класс CompletableFuture
определяет множество вариаций на основе методов, используемых в этих примерах. Перед использованием класса CompletableFuture
в своих приложениях изучите полный список методов завершения и методов объединения, чтобы найти метод, который наилучшим образом соответствует вашим потребностям.
Класс CompletableFuture
лучше всего подходит для случаев, когда вы применяете различные типы операций и хотите координировать их результаты. Если вы осуществляете одно и то же вычисление со многими различными значениями данных, параллельные потоки проще и, весьма вероятно, выигрывают в производительности. Для примера с проверкой расстояния редактирования лучше подходит вариант на основе параллельных потоков.
Потоки
Потоки, важное новое средство Java 8, работают совместно с лямбда-выражениями. По существу потоки – это push-итераторы, применяемые к последовательности значений. Потоки можно связывать в цепочку через адаптеры для исполнения таких операций, как фильтрация и преобразование (примерно как последовательности в Scala). Кроме того, потоки имеют последовательные и параллельные разновидности, что также напоминает последовательности Scala (следует, однако, отметить, что в Scala имеется отдельная иерархия классов для параллельных последовательностей, а в Java 8 используется внутренний флаг для обозначения категории потока: последовательный или параллельный). Существуют разновидности потоков для примитивных типов int
, long
и double
, а также потоков для типизированных объектов.
Новый API-интерфейс streams слишком сложен для полного рассмотрения в этой статье, поэтому я сосредоточусь на аспектах одновременного исполнения. Для получения более подробных сведений о потоках обратитесь в раздел Ресурсы.
В листинге 5 показан еще один вариант программного кода для отыскания наилучшего соответствия по расстоянию редактирования. Эта версия использует класс ChunkDistanceChecker
из листинга 1 для вычисления расстояния и класс CompletableFuture
из примера в листинге 2, однако на этот раз для нахождения наилучшего соответствия я использую потоки.
Листинг 5. Класс CompletableFuture
с использованием потоков
public class CompletableFutureStreamDistance extends TimingTestBase { private final List<ChunkDistanceChecker> chunkCheckers; ... public DistancePair bestMatch(String target) { return chunkCheckers.stream() .map(checker -> CompletableFuture.supplyAsync(() -> checker.bestDistance(target))) .collect(Collectors.toList()) .stream() .map(future -> future.join()) .reduce(DistancePair.worstMatch(), (a, b) -> DistancePair.best(a, b)); } }
Всю работу делает состоящий из нескольких строк оператор в нижней части листинга 5, использующий API-интерфейс streams.
chunkCheckers.stream()
создает поток изList<ChunkDistanceChecker>
..map(checker -> ...
применяет отображение к значениям в потоке (в этом случае с использованием того же подхода, что и в примере в листинге 2) с целью созданияCompletableFuture
для результата асинхронного исполнения методаChunkDistanceChecker.bestDistance()
..collect(Collectors.toList())
собирает значения в список, который.stream()
возвращает обратно в поток..map(future -> future.join())
ждет доступности результата каждого future, а.reduce(...
находит наилучшее значение посредством многократного применения методаDistancePair.best()
к предшествующему наилучшему результату и к самому последнему результату.
Возможно, это выглядит запутанно. Однако не прекращайте чтение статьи — уверяю вас, что следующий вариант кода будет "чище" и проще. Цель кода в листинге 5 состоит в том, чтобы продемонстрировать возможность использования потоков в качестве альтернативы обычным циклам.
Код в листинге 5 был бы гораздо проще без многократных преобразований из потоков в списки и обратно в потоки. В данном случае преобразование необходимо, поскольку в противном случае код ждал бы лишь завершения метода CompletableFuture.join()
сразу же после создания future-объекта.
Параллельные потоки
К счастью, существует более удобный способ реализации параллельных операций с потоками, чем громоздкий подход, показанный в листинге 5. Последовательные потоки можно превратить в параллельные потоки, а параллельные потоки автоматически распределяют работу между несколькими потоками и позволяют собирать их результаты на более позднем этапе. В листинге 6 демонстрируется применение этого подхода для нахождения наилучшего соответствия в списке List<ChunkDistanceChecker>
.
Листинг 6. Отыскание наилучшего соответствия с использованием параллельных потоков для фрагментов
public class ChunkedParallelDistance extends TimingTestBase { private final List<ChunkDistanceChecker> chunkCheckers; ... public DistancePair bestMatch(String target) { return chunkCheckers.parallelStream() .map(checker -> checker.bestDistance(target)) .reduce(DistancePair.worstMatch(), (a, b) -> DistancePair.best(a, b)); } }
В этом случае всю работу также делает состоящий из нескольких строк оператор в нижней части листинга. Как и в листинге 5, сначала из списка создается поток, однако в этой версии для получения потока используется метод parallelStream()
, ориентированный на параллельную обработку (вы также можете преобразовать обычный поток для параллельной обработки, вызвав метод потока parallel()
). Следующая часть кода, .map(checker -> checker.bestDistance(target))
, находит наилучшее соответствие внутри фрагмента, состоящего из известных слов. Последняя часть кода, .reduce(...
, аккумулирует наилучший результат для всех фрагментов, как это делается в листинге 5.
Параллельные потоки исполняют определенные шаги, такие как операции map
и filter
, параллельно. Таким образом, "за кадром" код в листинге 6 распространяет шаг map на несколько потоков, прежде чем консолидировать результаты на шаге reduce (это не обязательно делать в каком-либо определенном порядке, поскольку результаты поступают из операций, исполняемых параллельно).
Возможность разделения работы, подлежащей исполнению в потоке, обеспечивается новым интерфейсом
java.util.Spliterator<T>
, используемым в потоках. Класс Spliterator
, как можно предположить по названию, подобен классу
Iterator
. Spliterator
, как и Iterator
, позволяет работать с элементами коллекции поочередно — хотя вместо получения элементов из Spliterator
вы применяете действие к элементам с использованием метода tryAdvance()
или метода forEachRemaining()
. Однако Spliterator
также предоставляет оценку количества содержащихся в нем элементов и потенциально может быть разделен на две части (подобно делению биологической клетки). Эти добавленные возможности облегчают коду параллельной обработки потоков работ (stream) распределение работы между доступными процессорными потоками (thread).
Код в листинге 6 может показаться вам знакомым, поскольку он очень похож на пример параллельных коллекций в Scala из первой статьи данного цикла:
def bestMatch(target: String) = matchers.par.map(m => m.bestMatch(target)). foldLeft(DistancePair.worstMatch)((a, m) => DistancePair.best(a, m))
Можно увидеть некоторые различия в синтаксисе и в работе, однако по существу код параллельных потоков в Java 8 делает то же самое, что и код параллельных коллекций Scala, и таким же образом.
Порядок применения потоков
До сих пор во всех примерах задача сравнения сохраняла разделенную на фрагменты структуру, представленную в первой статье данного цикла. Это было необходимо для эффективного манипулирования параллельными задачами в более старых версиях Java. Механизм параллельных потоков Java 8 спроектирован для самостоятельного разделения работы, поэтому вы можете передать набор значений для обработки в виде потока, а встроенная обработка одновременного исполнения разделит этот набор с целью распределения работы между доступными процессорами.
При попытке применения этого подхода к задаче вычисления расстояния редактирования возникает несколько проблем. Если вы связываете шаги обработки в цепочку в виде конвейера (официальный термин для последовательности потоковых операций), то вы можете следующему этапу конвейера передать только один результат каждого шага. Если вам требуется несколько результатов (таких как значение наилучшего расстояния и соответствующее известное слово, использованное в задаче вычисления расстояния редактирования), их необходимо передавать как объект. Однако создание такого объекта для результата каждого отдельного сравнения снизило бы производительность "прямого" потокового подхода по сравнению с "фрагментированными" подходами. Что еще хуже, вычисление расстояния редактирования многократно использует два выделенных массива. Эти массивы не могут быть совместно использованы параллельными вычислениями, поэтому для каждого вычисления их необходимо выделять заново.
К счастью, API-интерфейс streams предоставляет эффективный способ обработки этой ситуации, хотя и ценой некоторых дополнительных усилий. В листинге 7 демонстрируется использование потока для обработки полного набора вычислений без создания промежуточных объектов или избыточных копий рабочих массивов.
Листинг 7. Потоковая обработка отдельных сравнений расстояния редактирования
public class NonchunkedParallelDistance extends TimingTestBase { private final String[] knownWords; ... private static int editDistance(String target, String known, int[] v0, int[] v1) { ... } public DistancePair bestMatch(String target) { int size = target.length() + 1; Supplier<WordChecker> supplier = () -> new WordChecker(size); ObjIntConsumer<WordChecker> accumulator = (t, value) -> t.checkWord(target, knownWords[value]); BiConsumer<WordChecker, WordChecker> combiner = (t, u) -> t.merge(u); return IntStream.range(0, knownWords.length).parallel() .collect(supplier, accumulator, combiner).result(); } private static class WordChecker { protected final int[] v0; protected final int[] v1; protected int bestDistance = Integer.MAX_VALUE; protected String bestKnown = null; public WordChecker(int length) { v0 = new int[length]; v1 = new int[length]; } protected void checkWord(String target, String known) { int distance = editDistance(target, known, v0, v1); if (bestDistance > distance) { bestDistance = distance; bestKnown = known; } else if (bestDistance == distance) { bestKnown = null; } } protected void merge(WordChecker other) { if (bestDistance > other.bestDistance) { bestDistance = other.bestDistance; bestKnown = other.bestKnown; } else if (bestDistance == other.bestDistance) { bestKnown = null; } } protected DistancePair result() { return (bestKnown == null) ? new DistancePair(bestDistance) : new DistancePair(bestDistance, bestKnown); } } }
В листинге 7 для объединения результатов используется мутабельный контейнерный класс результатов (в данном случае — это класс WordChecker
). Метод bestMatch()
реализует сравнение с использованием трех "механизмов" в форме лямбда-выражений.
- Лямбда-выражение
Supplier<WordChecker> supplier
предоставляет экземпляры контейнера результатов. - Лямбда-выражение
ObjIntConsumer<WordChecker> accumulator
помещает новое значение в контейнер результатов. - Лямбда-выражение
BiConsumer<WordChecker, WordChecker> combiner
объединяет два контейнера результатов с целью получения объединенного значения.
После определения трех лямбда-выражений заключительный оператор метода bestMatch()
создает параллельный поток int
-значений для индексов в массиве известных слов и направляет этот поток в метод IntStream.collect()
. Метод collect()
вызывает эти три лямбда-выражения для фактического выполнения всей работы.
Производительность одновременного исполнения в Java 8
На рис. 1 показано, как меняется измеряемая производительность в зависимости от размера фрагмента при исполнении тестового кода на моей четырехъядерной системе AMD с Oracle Java 8 под управлением 64-разрядной Linux®. Как и при определении времени исполнения в первой статье данного цикла, каждое входящее слово поочередно сравнивается с 12564 известными словами, а каждая задача находит наилучшее соответствие внутри набора известных слов. Весь набор из 933 входящих слов с орфографическими ошибками обрабатывается многократно; с паузами между проходами для стабилизации состояния JVM. Наилучшее время после 10 проходов используется в графике, показанном на рис. 1. Заключительный размер блока 16384 превышает количество известных слов, таким образом, этот случай демонстрирует однопоточную производительность. В этот тест на время исполнения включены следующие реализации: четыре основных варианта из этой статьи и лучший вариант из первой статьи.
- CompFuture:
CompletableFutureDistance0
из листинга 2 - CompFutStr:
CompletableFutureStreamDistance
из листинга 5 - ChunkPar:
ChunkedParallelDistance
из листинга 6 - ForkJoin:
ForkJoinDistance
из листинга 3 первой статьи - NchunkPar:
NonchunkedParallelDistance
из листинга 7
Рисунок 1. Производительность в Java 8

Кликните, чтобы увидеть увеличенное изображение
Рис. 1 демонстрирует впечатляющие результаты нового подхода Java 8 на основе параллельных потоков; особенно выделяется полностью оптимизированная реализация
NchunkPar
, показанная в листинге 7. Оптимизация, избавляющая от необходимости создания объектов, дает для времени исполнения результат (на графике показано только одно значение, поскольку в данном подходе фрагменты не используются), соответствующий максимальной производительности среди всех других альтернативных вариантов. Подходы CompletableFuture
немного слабее с точки зрения производительности, но это не является неожиданностью, поскольку в этом примере не используются сильные стороны этого класса. В листинге 5 время исполнения для ChunkPar
приблизительно соответствует результату для кода ForkJoin
из первой статьи, хотя при меньшей чувствительности к размерам фрагмента. Все разновидности, которые поочередно проверяют фрагменты слов, при небольших размерах фрагмента демонстрируют снижение производительности относительно прогнозируемых показателей, поскольку растет роль относительных издержек на создание объектов в сравнении с фактической вычислительной работой.
Как и результаты из первой статьи, вышеприведенные результаты дают общее представление о производительности, которой можно ожидать в приложениях. Самый важный вывод состоит в том, что новые параллельные потоки Java 8 обеспечивают превосходную производительность при надлежащем применении. Объедините хорошую производительность с преимуществами функционального стиля кодирования потоков в разработке — и у вас на руках будет выигрышная комбинация на любой случай, когда вам потребуются вычисления над коллекциями значений.
Резюме одновременного исполнения в Java 8
Java 8 добавляет в инструментарий разработчика несколько новых важных возможностей. Если говорить об одновременном исполнении, реализация на основе параллельная потоков быстра и проста в использовании, особенно в сочетании с лямбда-выражениями для функционального стиля программирования, который ясно и кратко выражает намерения разработчика. Новый класс CompletableFuture
также упрощает программирование одновременного исполнения, когда вы работаете с отдельными действиями, которые плохо подходят для потоковой модели.
Следующая статья в цикле Одновременное исполнение на платформе JVM будет посвящена языку Scala. В ней будет рассмотрен иной — и весьма интересный — способ манипулирования асинхронными вычислениями. Макрос async
позволяет вам написать код, который ведет себя так, как будто вы выполняете последовательные операции блокирования, но где-то внутри Scala преобразует этот код в совершенно неблокирующую структуру. Я продемонстрирую полезность этой возможности на нескольких примерах и рассмотрю ее реализацию. Кто знает — возможно, какая-то часть этой новой работы с языком Scala со временем реализуется в языке Java 9.
Ресурсы для скачивания
Похожие темы
- Оригинал статьи: JVM concurrency: Java 8 concurrency basics
- Демонстрационный программный код для данной статьи. Загрузите полный демонстрационный код для данной статьи из репозитария автора на сайте GitHub.
- Scalable Scala: Денис Сосноски, автор данного цикла, делится опытом и внутренней информацией о содержании курса и о разработке на языке Scala в целом.
- Java 8 language changes (Изменения в языке Java 8), Денис Сосноски (Dennis Sosnoski), developerWorks, апрель 2014 г. Рассматриваются языковые изменения в Java 8 (лямбда-выражения и интерфейсы).
- Java 8: Definitive guide to CompletableFuture Томаш Нуркевич (Tomasz Nurkiewicz). Экскурсия по полному API-интерфейсу
CompletableFuture
, включая его сравнение с эквивалентами на других языках JVM. - Aggregate Operations: раздел Java Tutorial, посвященный использованию потоков в Java 8, в том числе их параллельному исполнению.
- Streams in Top Gear: презентация на конференции JavaOne 2013, посвященная углубленному исследованию архитектуры API-интерфейса streams и его реализации, включая рекомендации по достижению максимальной производительности и масштабируемости с помощью библиотеки streams.
- Concurrency JSR-166 Interest Site: этот веб-сайт, поддерживаемый Дугом Ли (Doug Lea), посвящен работе над поддержкой одновременного исполнения в Java, которая впервые была реализована в выпуске JSR 166.
- Записная книжка дизайнера языка: в этом цикле статей developerWorks архитектор языка Java Брайан Гетц исследует некоторые проблемы дизайна языка, вызвавшие трудности при эволюции языка Java к версиям Java SE 7, Java SE 8 и далее.
- IBM SDK, Java Technology Edition Version 8: Примите участие в программе бета-тестирования IBM SDK for Java 8.0.
- IBM Java developer kits: Найдите пакет IBM Java SDK и среду исполнения для своей платформы.