Автор статьи: Андрей Поляков

Старший разработчик в Unlimint

Одна из ключевых особенностей (и возможно даже фичей java) - мощная поддержка потоков операционной системы на уровне jvm и удобные механизмы работы с ними.

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

  • запуск нескольких процессов (например, с помощью fork)

  • запуск потоков (threads)

  • так называемые green threads (или корутины) - физически один поток, в рамках которого запускается несколько асинхронных задач.

Последний подход с green threads получил особое распространение в java script в связи с особенностями браузеров - весь код выполняется в рамках одного потока. В целом подход очень интересный и хотелось бы применять, в том числе и в java.

Project Loom — это попытка сообщества OpenJDK представить облегченную конструкцию параллелизма в Java.

Хотя для Loom еще нет запланированного релиза, мы можем получить доступ к последним прототипам на вики Project Loom: https://wiki.openjdk.org/display/loom .

Модель параллелизма в Java

Прежде чем мы обсудим различные концепции Loom, давайте обсудим текущую модель параллелизма в Java.

Поскольку Java использует для реализации параллелизма потоки ядра ОС, она не соответствует современным требованиям параллелизма. В частности, есть две основные проблемы:

  • Потоки могут соответствовать масштабу единицы параллелизма. Например, приложения обычно допускают до миллионов транзакций, пользователей или сеансов. Однако количество потоков, поддерживаемых ядром ОС, намного меньше. Таким образом, поток для каждого пользователя, транзакции или сеанса часто невозможен.

  • Большинству параллельных приложений требуется некоторая синхронизация между потоками для каждого запроса. Из-за этого между потоками ОС происходит дорогостоящее переключение контекста (а если потоков много? то тогда все очень плохо).

Возможным решением таких проблем является использование асинхронных параллельных API. Распространенными примерами являются CompletableFuture и RxJava. При условии, что такие API не блокируют поток ядра, они предоставляют приложению более удобную и масштабируемую конструкцию параллелизма поверх потоков Java.

С другой стороны, такие API сложнее отлаживать и интегрировать с легаси API. Таким образом, требуется облегченная библиотека, независимая от потоков ядра. 

Любая реализация потока, легковесная или тяжеловесная, зависит от двух конструкций:

  • Task — последовательность инструкций, которая может приостанавливаться для некоторой блокирующей операции.

  • Scheduler — ставит на выполение задачи и переключает CPU на выполнение следующей задачи (для этого нужно хранить стек вызовов!).

Так как текущая реализация JDK использует реализацию потоков ОС, которая включает собственный стек вызовов, то  вместе со стеком вызовов Java, это приводит к большому объему памяти, который нужен.

Однако куда более серьезной проблемой является использование планировщика ОС. Поскольку планировщик работает в режиме ядра, между потоками нет различий. И он обрабатывает каждый запрос процессора одинаково (неважно, относится он к JVM, или нет).

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

Project Loom предлагает решить эту проблему с помощью потоков пользовательского уровня, которые полагаются на реализацию задач и планировщиков внутри  Java, а не на реализацию ОС.

Project Loom

Будем рассматривать Project Loom на основе dk 19.
В центре стоит концепция виртуальных потоков - fiber-ы.

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    IntStream.range(0, 100_000).forEach(i -> executor.submit(() -> { 
        Thread.sleep(Duration.ofSeconds(1));
        System.out.println(i);
        return i;
    }));
}

Используется метод newVirtualThreadPerTaskExecutor для создания executor-а.

Библиотека для Fiber-ов аналогична старым добрым Thread. Но есть два ключевых отличия:

  • Fiber оборачивает задачи  в некоторый промежуточный вид, для запуска в  пользовательском режиме. Это позволило бы задаче приостанавливаться и возобновляться в среде выполнения Java, а не в ядре ОС.

  • Будет использоваться планировщик пользовательского уровня (например, ForkJoinPool).

Другой пример (самостоятельно создаем VirtualThreadExecutor с помощью конструктора):

var executor = new VirtualThreadExecutor();
    var future = executor.submit(() -> {
      Thread.sleep(100);
      return "hello";
    });
    executor.shutdown();

Есть также класс, который является корутиной (Continuation):

Continuation cont1 = new Continuation(SCOPE_CONT_1, () -> {
    Continuation cont2 = new Continuation(SCOPE_CONT_2. () -> {
        //do something
        suspend(SCOPE_CONT_2);
        suspend(SCOPE_CONT_1);
    });
});

Давайте рассмотрим пример пошагово:

var continuation = new Continuation(() -> {
      builder.append("continuation -- start\n");
      Continuation.yield();
      builder.append("continuation -- middle\n");
      Continuation.yield();
      builder.append("continuation -- end\n");
 });
builder.append("main -- before start\n");
continuation.run();
builder.append("main -- after start\n");
builder.append("main -- before start 2\n");
continuation.run();
 builder.append("main -- after start 2\n");
builder.append("main -- before start 3\n");
continuation.run();
 builder.append("main -- after start 3\n");

(1) - создаем корутину
(2) - запускаем ее
(3) - внутри виртуального потока мы приостанавливаем выполнение корутины
(4) - главный поток продолжает выполняться и снова запускает корутину
(5) - виртуальный поток продолжает выполняться и снова приостанавливается
(6) -  главный поток продолжает выполняться и снова запускает корутину
(7) - виртуальный поток продолжает выполняться и завершается
(8) - главный поток завершается

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

Как запустить

Так как Loom - это экспериментальная фича, то нельзя просто так взять и запустить приведенный выше код.

Вам понадобится скачать экспериментальный JDK (19 и новее). Превью-билды можно найти здесь: https://jdk.java.net/21/ 

Если просто запустить код выше, то получим ошибку:

C:\Users\A.Polyakov\loom-example\src\main\java\org\example\Main.java:8:20
java:experimental feature Continuation only supported in preview build

Далее в зависимости от способа запуска, нужно настроить секцию build в maven:

<build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>19</source>
                    <target>19</target>
                    <compilerArgs>
                        <arg>--enable-preview</arg>
                    </compilerArgs>
                </configuration>
            </plugin>
        </plugins>
    </build>

При запуске в Intellij IDEA нужно включить поддержку экспериментальных фичей, например, так:

<component name="JavacSettings">
    <option name="ADDITIONAL_OPTIONS_OVERRIDE">
      <module name="loom-example" options="--enable-preview --add-exports java.base/jdk.internal.vm=ALL-UNNAMED" />
    </option>
</component>

Убедитесь, что в настройках модуля включена поддержка экспериментальных фичей:

Что в итоге

Библиотека Loom выглядит очень перспективной, API удобное и в Java корутинов очень не хватает.

Будем ждать, когда Loom выйдет из статуса экспериментальной фичи и появится в stable версии java.

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

Урок будет полезен всем начинающим Java-разработчикам.

Комментарии (0)