JavaRush /Курсы /JAVA 25 SELF /Масштабируемость и производительность Virtual Threads

Масштабируемость и производительность Virtual Threads

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

1. Масштабируемость

Почему обычные потоки плохо масштабируются?

Каждый классический поток (Thread) — это сущность операционной системы с собственным стеком (обычно 12 МБ) и структурой состояния. Попытка создать, скажем, 10 000 обычных потоков нередко приводит к ошибке OutOfMemoryError. Поэтому в традиционных серверах используют ограниченные пулы потоков.

Виртуальные потоки: магия масштабируемости

Виртуальные потоки (Java 21+) — «лёгкие» потоки, управляемые JVM. Их стек хранится в куче и может динамически расти/сжиматься. Когда поток блокируется на I/O, JVM «замораживает» его и продолжает работу других задач.

Внутри JVM работает небольшой пул «носителей» (carrier threads) — платформенных потоков ОС, на которых по очереди исполняются виртуальные потоки. Это позволяет создавать 100 000+ задач без ужаса по памяти. JVM сама планирует, какие виртуальные потоки когда исполнять.

Демонстрация: 100_000 виртуальных потоков против 1_000 платформенных

Пример: создание 1000 обычных потоков

// Попытка создать 1000 обычных потоков
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
    Thread t = new Thread(() -> {
        try {
            Thread.sleep(10_000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    });
    threads.add(t);
    t.start();
}
System.out.println("Создано потоков: " + threads.size());

Результат: На большинстве систем получится создать 1 0002 000 потоков; при больших значениях начнутся проблемы с памятью и тормоза.

Пример: создание 100 000 виртуальных потоков

// Создаём 100_000 виртуальных потоков
List<Thread> vThreads = new ArrayList<>();
for (int i = 0; i < 100_000; i++) {
    Thread t = Thread.ofVirtual().start(() -> {
        try {
            Thread.sleep(10_000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    });
    vThreads.add(t);
}
System.out.println("Создано виртуальных потоков: " + vThreads.size());

Результат: Программа спокойно создаёт 100 000 виртуальных потоков без падений и существенных тормозов. Памяти требуется кратно меньше.

Визуальное сравнение

Тип потока Максимально потоков (примерно) Использование памяти Время запуска
Обычные (Thread) 1 00010 000 Высокое Долго
Виртуальные 100 0001 000 000+ Низкое Мгновенно

Факт: Виртуальные потоки позволяют писать код «один поток на задачу» без сложных пулов и риска перегрузки системы.

2. Производительность: где виртуальные потоки раскрывают себя

Задачи, которые «упираются» в ввод-вывод (I/O-bound)

Виртуальные потоки отлично подходят для сетевых запросов, файлового I/O и работы с БД. Когда операция блокируется, виртуальный поток освобождает «носителя», а JVM исполняет другие задачи. Это повышает пропускную способность при большом числе одновременных ожиданий.

Пример: имитация 10 000 одновременных HTTP-запросов

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

HttpClient client = HttpClient.newHttpClient();
List<Thread> threads = new ArrayList<>();

for (int i = 0; i < 10_000; i++) {
    Thread t = Thread.ofVirtual().start(() -> {
        try {
            HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://example.com"))
                .build();
            HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
            System.out.println("Ответ: " + response.statusCode());
        } catch (Exception e) {
            System.out.println("Ошибка: " + e.getMessage());
        }
    });
    threads.add(t);
}

// Ждём завершения всех потоков
for (Thread t : threads) {
    t.join();
}

Результат: Все 10 000 запросов выполняются параллельно; программа не падает, а код остаётся простым.

CPU-bound: виртуальные потоки не ускоряют вычисления

Если задача нагружает CPU, виртуальные потоки не добавят скорости: количество ядер фиксировано. Здесь уместны фиксированные пулы, равные числу ядер, чтобы не создавать лишнюю конкуренцию.

// Каждая задача считает сумму большого диапазона
Runnable cpuTask = () -> {
    long sum = 0;
    for (int i = 0; i < 100_000_000; i++) {
        sum += i;
    }
    System.out.println("Сумма: " + sum);
};

// Запускаем 1000 виртуальных потоков с вычислениями
for (int i = 0; i < 1000; i++) {
    Thread.ofVirtual().start(cpuTask);
}

Результат: Потоки будут конкурировать за CPU, но ускорения не будет — это не задача для Virtual Threads.

3. Ограничения и особенности

Синхронизация и ловушки виртуальных потоков

  • Осторожно с нативными блокировками. Использование synchronized может «приклеить» виртуальный поток к носителю, снижая выгоду. Предпочитайте ReentrantLock, Semaphore и другие примитивы из java.util.concurrent, оптимизированные под виртуальные потоки.
  • Старые библиотеки. Некоторые JDBC-драйверы и нативные библиотеки ещё не оптимизированы под Virtual Threads. Тщательно тестируйте блокирующие операции.

Не для долгоживущих задач

Virtual Threads идеальны для «коротких» единиц работы: обработка запроса, одна операция и завершение. Миллион бесконечно живущих задач (например, вечные вычисления) не принесут пользы — для них используйте платформенные потоки.

4. Best practices: где использовать виртуальные потоки

  • I/O-bound задачи: сетевые вызовы, файлы, БД — всё, где поток часто ждёт.
  • Web-серверы: обрабатывайте каждый HTTP-запрос в отдельном виртуальном потоке.
  • Интеграционные тесты: быстро имитируйте тысячи клиентов.
  • Асинхронная обработка: пишите привычный «блокирующий» код — JVM выполняет умное планирование.

Не стоит использовать:

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

Под капотом удобно использовать исполнитель: Executors.newVirtualThreadPerTaskExecutor() — «один виртуальный поток на задачу», без фиксированного пула.

5. Мониторинг и измерение: как увидеть виртуальные потоки в деле

JVisualVM и Flight Recorder

JVisualVM покажет активные потоки, их состояния и память; начиная с Java 21 виртуальные потоки отображаются отдельно. Java Flight Recorder (JFR) пишет подробный «чёрный ящик» исполнения, включая статистику по Virtual Threads — удобно для поиска узких мест.

Как посмотреть количество потоков в коде

Простой способ увидеть количество потоков в JVM:

System.out.println("Всего потоков: " + Thread.activeCount());

Посчитать, сколько из них виртуальные:

long vCount = Thread.getAllStackTraces().keySet().stream()
    .filter(Thread::isVirtual)
    .count();
System.out.println("Виртуальных потоков: " + vCount);

6. Типичные ошибки при работе с виртуальными потоками

Ошибка №1: Использование виртуальных потоков для тяжёлых вычислений. Запуск миллионов виртуальных потоков с CPU-bound задачами не ускорит процессор. Virtual Threads — не «турбо» для вычислений.

Ошибка №2: Слепое копирование старых паттернов. Не создавайте фиксированные пулы виртуальных потоков. Используйте Executors.newVirtualThreadPerTaskExecutor() и позвольте JVM масштабироваться автоматически.

Ошибка №3: Использование неподдерживаемых библиотек. Нативные блокировки и библиотеки, не адаптированные под Loom, могут давать подвисания и просадки. Проверяйте совместимость заранее.

Ошибка №4: Преждевременная оптимизация. Если у вас несколько потоков и обычная многопоточность — не спешите переносить всё на Virtual Threads. Инструмент хорош там, где есть массовый I/O и ожидание.

Ошибка №5: Игнорирование мониторинга. Создать миллион задач легко, но без мониторинга и обработки исключений можно получить «красивый бенчмарк» вместо надёжной системы. Используйте JVisualVM и JFR.

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