Рубрики
Без рубрики

Разница между потоком и виртуальным потоком в Java

Быстрое и практичное сравнение потоков и виртуальных потоков в Java.

Автор оригинала: Denis Szczukocki.

1. введение

В этом уроке мы покажем разницу между традиционными потоками в Java и виртуальными потоками, представленными в Project Loom .

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

Прежде чем мы начнем, мы должны отметить, что этот проект находится в активной разработке. Мы запустим наши примеры на виртуальной машине раннего доступа look: openjdk-15-look+4-55_windows-x64_bin.

Более новые версии сборок могут свободно изменять и ломать текущие API. Тем не менее, в API уже произошли серьезные изменения, как и в ранее использовавшемся java.lang.Класс Fiber был удален и заменен новым классом java.lang.VirtualThread .

2. Обзор потоков на высоком уровне по сравнению с Виртуальный поток

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

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

Затем, как правило, мы не хотим блокировать эти потоки, и это приводит к использованию неблокирующих API ввода-вывода и асинхронных API, которые могут загромождать наш код.

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

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

В конечном счете, нам не нужно будет обращаться к NIO или асинхронным API. Это должно привести к более удобочитаемому коду, который легче понять и отладить. Тем не менее, продолжение потенциально может блокировать поток — носитель – в частности, когда поток вызывает собственный метод и выполняет операции блокировки оттуда.

3. Новый API построителя потоков

В Loom мы получили новый API builder в классе Thread , а также несколько заводских методов. Давайте посмотрим, как мы можем создавать стандартные и виртуальные фабрики и использовать их для выполнения наших потоков:

Runnable printThread = () -> System.out.println(Thread.currentThread());
        
ThreadFactory virtualThreadFactory = Thread.builder().virtual().factory();
ThreadFactory kernelThreadFactory = Thread.builder().factory();

Thread virtualThread = virtualThreadFactory.newThread(printThread);
Thread kernelThread = kernelThreadFactory.newThread(printThread);

virtualThread.start();
kernelThread.start();

Вот результат приведенного выше прогона:

Thread[Thread-0,5,main]
VirtualThread[,ForkJoinPool-1-worker-3,CarrierThreads]

Здесь первая запись-это стандартный вывод toString потока ядра.

Теперь мы видим в выходных данных, что виртуальный поток не имеет имени, и он выполняется в рабочем потоке ForkJoinPool из группы CarrierThreads thread.

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

Кроме того, нам не нужно изучать новый API, чтобы использовать их.

4. Состав виртуального Потока

Это продолжение и планировщик , которые вместе составляют виртуальный поток. Теперь наш планировщик пользовательского режима может быть любой реализацией интерфейса Executor . Приведенный выше пример показал нам, что по умолчанию мы запускаем ForkJoinPool .

Теперь, подобно потоку ядра, который может быть выполнен на процессоре, затем припаркован, перенесен обратно и затем возобновлен, продолжение – это блок выполнения, который может быть запущен, затем припаркован (возвращен), перенесен обратно и возобновлен таким же образом с того места, где он остановился, и все еще управляется JVM вместо того, чтобы полагаться на операционную систему.

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

Однако, чтобы показать, как это работает под капотом, теперь мы запустим наше экспериментальное продолжение:

var scope = new ContinuationScope("C1");
var c = new Continuation(scope, () -> {
    System.out.println("Start C1");
    Continuation.yield(scope);
    System.out.println("End C1");
});

while (!c.isDone()) {
    System.out.println("Start run()");
    c.run();
    System.out.println("End run()");
}

Вот результат приведенного выше прогона:

Start run()
Start C1
End run()
Start run()
End C1
End run()

В этом примере мы запустили наше продолжение и в какой-то момент решили остановить обработку. Затем, как только мы снова запустили его, наше продолжение продолжилось с того места, где оно закончилось. По выходным данным мы видим, что метод run() был вызван дважды, но продолжение было запущено один раз, а затем продолжило свое выполнение на втором запуске с того места, где оно остановилось.

Именно так операции блокировки должны обрабатываться JVM. Как только произойдет операция блокировки, продолжение завершится, оставив поток-носитель разблокированным.

Итак, произошло то, что наш основной поток создал новый кадр стека в своем стеке вызовов для метода run() и продолжил выполнение. Затем, после завершения продолжения, JVM сохранила текущее состояние своего выполнения.

Затем основной поток продолжил свое выполнение, как если бы метод run() вернулся и продолжил цикл while . После второго вызова метода continuations run JVM восстановила состояние основного потока до точки, в которой продолжение дало результат, и завершила выполнение.

5. Заключение

В этой статье мы обсудили разницу между потоком ядра и виртуальным потоком. Затем мы показали, как мы можем использовать новый API построителя потоков из Project Loom для запуска виртуальных потоков.

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