JavaRush /Курсы /JAVA 25 SELF /Создание виртуальных потоков: Thread.ofVirtual().start()

Создание виртуальных потоков: Thread.ofVirtual().start()

JAVA 25 SELF
57 уровень , 1 лекция
Открыта

1. Как создавать виртуальные потоки на практике

Пора переходить от теории к практике! Вы уже знаете, как создаётся поток в классике:

Thread t = new Thread(() -> System.out.println("Hello from thread!"));
t.start();

Или чуть короче:

new Thread(() -> System.out.println("Hi!")).start();

Теперь с Java 21 у нас появился новый способ:

Thread.startVirtualThread(() -> System.out.println("Hello from virtual thread!"));

или более явно:

Thread t = Thread.ofVirtual().start(() -> System.out.println("Hello from virtual thread!"));

В чём разница?

  • Thread.ofVirtual().start(...) создаёт виртуальный поток (Virtual Thread), управляемый JVM, а не ОС.
  • Thread.ofPlatform().start(...) (или new Thread(...)) — классический поток, как раньше.

Почему это важно?

Виртуальные потоки можно создавать десятками тысяч, не опасаясь OutOfMemoryError. Теперь, если вы вдруг решите обработать миллион запросов — Java скажет: «Без проблем, давай ещё!»

2. Синтаксис создания виртуального потока

Базовый пример:

public class VirtualThreadDemo {
    public static void main(String[] args) {
        Thread thread = Thread.ofVirtual().start(() -> {
            System.out.println("Привет из виртуального потока! Поток: " + Thread.currentThread());
        });
        // Ждём завершения потока (чтобы main не завершился раньше)
        try {
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Что происходит?

  • Мы создаём виртуальный поток через Thread.ofVirtual().start(...).
  • Внутри потока — простое действие: печать сообщения.
  • В конце вызываем thread.join(), чтобы основной поток подождал завершения виртуального (иначе программа может завершиться раньше, чем поток успеет что-то вывести).

Обратите внимание:
Виртуальный поток выглядит и ведёт себя почти как обычный, но внутри — магия JVM!

3. Массовое создание виртуальных потоков: сила Loom на практике

А теперь попробуем то, что с обычными потоками было бы рискованно (или просто невозможно): создадим 10_000 виртуальных потоков, каждый из которых напишет свой номер.

public class VirtualThreadMassive {
    public static void main(String[] args) throws InterruptedException {
        int N = 10_000;
        Thread[] threads = new Thread[N];

        for (int i = 0; i < N; i++) {
            int threadNum = i;
            threads[i] = Thread.ofVirtual().start(() -> {
                System.out.println("Виртуальный поток #" + threadNum + " работает!");
            });
        }

        // Ждём завершения всех потоков
        for (Thread t : threads) {
            t.join();
        }
        System.out.println("Все виртуальные потоки завершены!");
    }
}
  • Для обычных потоков (new Thread(...)) такой код почти гарантированно «уронит» вашу программу с OutOfMemoryError.
  • Для виртуальных потоков — это штатный режим! JVM с лёгкостью обработает тысячи и десятки тысяч потоков.

К слову, если вам кажется, что 10_000 — это много, попробуйте 100_000 или даже 1_000_000. На современной машине JVM справится, если ваши потоки выполняют простую работу или ожидают ввода-вывода.

4. Runnable и лямбда-выражения: как передавать код виртуальному потоку

Виртуальные потоки принимают задачи так же, как обычные потоки: через интерфейс Runnable. Это значит, что вы можете передавать и лямбда-выражения, и ссылку на метод, и любой объект, реализующий Runnable.

Пример с лямбдой:

Thread.ofVirtual().start(() -> System.out.println("Лямбда в виртуальном потоке!"));

Пример с методом:

public class TaskRunner {
    public static void main(String[] args) {
        Thread.ofVirtual().start(TaskRunner::doWork);
    }

    static void doWork() {
        System.out.println("Работаем в виртуальном потоке: " + Thread.currentThread());
    }
}

Пример с анонимным классом:

Thread.ofVirtual().start(new Runnable() {
    @Override
    public void run() {
        System.out.println("Анонимный класс в виртуальном потоке!");
    }
});

Вывод:
Всё, что работало с обычными потоками, работает и с виртуальными — только теперь это «легко и быстро».

5. Сравнение с ExecutorService: старый и новый подход

Классический ExecutorService

ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
    int taskNum = i;
    executor.submit(() -> {
        System.out.println("Задача #" + taskNum + " выполняется");
    });
}
executor.shutdown();

Проблема:
Если задач слишком много, а потоков мало — задачи ждут в очереди. Если потоков слишком много — программа «захлебнётся» от нехватки ресурсов.

Новый способ: Executor на виртуальных потоках

ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
for (int i = 0; i < 100_000; i++) {
    int taskNum = i;
    executor.submit(() -> {
        System.out.println("Виртуальная задача #" + taskNum);
    });
}
executor.shutdown();

Что происходит?

  • На каждую задачу создаётся отдельный виртуальный поток.
  • JVM сама управляет их планированием, не перегружая систему.
  • Нет нужды ограничивать размер пула — виртуальные потоки «почти бесплатны».

Когда использовать Executor на виртуальных потоках?

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

6. Практические советы: когда что использовать

Когда использовать Thread.ofVirtual().start() напрямую?

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

Когда использовать Executors.newVirtualThreadPerTaskExecutor()?

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

Совет:
Если не уверены — начинайте с Executor на виртуальных потоках. Это наиболее универсальный и современный способ.

7. Обработка исключений в виртуальных потоках

Виртуальные потоки — это обычные потоки с точки зрения try-catch. Если внутри вашего Runnable произойдёт исключение, оно не «сломает» всю JVM, а просто завершит данный поток с ошибкой.

Пример:

Thread t = Thread.ofVirtual().start(() -> {
    throw new RuntimeException("Что-то пошло не так!");
});
try {
    t.join();
} catch (InterruptedException e) {
    e.printStackTrace();
}
System.out.println("Основной поток продолжил работу.");

В ExecutorService:
Если отправляете задачу через submit, то результат можно получить через Future, и исключение будет проброшено при вызове get():

ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
Future<?> f = executor.submit(() -> {
    throw new RuntimeException("Ошибка в виртуальной задаче");
});
try {
    f.get();
} catch (ExecutionException e) {
    System.out.println("Поймали ошибку из виртуального потока: " + e.getCause());
}
executor.shutdown();

8. Типичные ошибки при создании виртуальных потоков

Ошибка №1: Путать виртуальные и платформенные потоки. Если вы создаёте потоки через new Thread(...) или Thread.ofPlatform(), это не виртуальные потоки. Только Thread.ofVirtual().start(...) или методы из Executors дают вам настоящие Virtual Threads.

Ошибка №2: Ожидать ускорения для тяжёлых вычислений. Виртуальные потоки не ускоряют задачи, сильно нагружающие процессор (CPU-bound). Если у вас миллион потоков, каждый из которых считает число Пи до миллионного знака — JVM не сможет «ускорить» вычисления, просто будет переключать потоки.

Ошибка №3: Держать ресурсы (например, базы данных) на каждый поток. Если вы создаёте миллион виртуальных потоков, но каждому нужно отдельное подключение к базе — база не выдержит. Виртуальные потоки хороши для задач, где основная часть времени — ожидание (I/O), а не работа с ограниченными внешними ресурсами.

Ошибка №4: Не дожидаться завершения потоков, если это важно. Если основной поток завершился раньше, чем виртуальные потоки — программа может завершиться, не дождавшись результатов. Используйте join() или ExecutorService с shutdown() и awaitTermination().

Ошибка №5: Использовать устаревшие библиотеки, не совместимые с виртуальными потоками. Некоторые сторонние библиотеки могут блокировать потоки на уровне ОС или использовать native-синхронизацию, что снижает эффективность виртуальных потоков. Всегда проверяйте совместимость.

1
Задача
JAVA 25 SELF, 57 уровень, 1 лекция
Недоступна
Телеметрия гоночного болида 🏎️
Телеметрия гоночного болида 🏎️
1
Задача
JAVA 25 SELF, 57 уровень, 1 лекция
Недоступна
Управление экспериментальным роботом-помощником 🤖
Управление экспериментальным роботом-помощником 🤖
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ