Как использовать виртуальные потоки
Короткий ответ
Виртуальные потоки (Java 21) очень лёгкие: их можно создавать тысячами. Проще всего — исполнитель, который запускает каждую задачу в новом виртуальном потоке:
void main() {
var done = new AtomicInteger();
long start = System.nanoTime();
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10_000; i++) {
executor.submit(() -> {
Thread.sleep(Duration.ofMillis(500));
return done.incrementAndGet();
});
}
}
long ms = (System.nanoTime() - start) / 1_000_000;
IO.println(done.get() + " tasks, less than 3 s: " + (ms < 3000));
}Результат:
10000 tasks, less than 3 s: true10 000 задач, каждая из которых ждёт полсекунды, завершаются примерно за полсекунды. С обычным пулом из 10 потоков это заняло бы больше восьми минут.
Один поток
Thread.ofVirtual().start() запускает один виртуальный поток, join() ждёт его завершения:
void main() throws InterruptedException {
Thread thread = Thread.ofVirtual().start(() -> IO.println("Hello from a virtual thread"));
thread.join();
IO.println(thread.isVirtual());
}Результат:
Hello from a virtual thread
trueКогда подходят
Виртуальные потоки хороши для задач, которые в основном ждут: запросы к сети, базе данных, файлам. Пока поток ждёт, JVM отдаёт процессор другим. Для тяжёлых вычислений они не ускоряют работу — число ядер процессора от них не растёт.
Частая ошибка
Складывать виртуальные потоки в пул фиксированного размера. Их не нужно экономить: создавайте новый поток на каждую задачу. Если нужно ограничить нагрузку, например число одновременных запросов к базе, используйте Semaphore.
Функции из этого ответа
- Executors.newVirtualThreadPerTaskExecutor() — Создаёт сервис, который запускает каждую задачу в отдельном виртуальном потоке.
- Thread.ofVirtual() — Создаёт виртуальный поток — лёгкий поток, которых можно запустить миллионы.
- Thread.sleep() — Приостанавливает текущий поток на заданное время.