Форум программистов, компьютерный форум, киберфорум
Javaican
Войти
Регистрация
Восстановить пароль

Многопоточность в Java с Project Loom: виртуальные или обычные потоки

Запись от Javaican размещена 15.03.2025 в 09:34
Показов 1635 Комментарии 0

Нажмите на изображение для увеличения
Название: b41d938b-2286-44d9-bca9-0aa5a5ffc4bf.jpg
Просмотров: 56
Размер:	294.9 Кб
ID:	10406
Многопоточность всегда была одноим из основных элементов в разработке современного программного обеспечения. Она позволяет приложениям обрабатывать несколько задач одновременно, что критично для создания отзывчивых и масштабируемых систем. Project Loom — амбициозный проект Oracle, который обещает революцию в многопоточности в Java. Вместо того чтобы предлагать очередную библиотеку или фреймворк, Loom вносит изменения в саму виртуальную машину Java, вводя концепцию виртуальных потоков.

В чем же ключевая разница? Традиционные потоки Java (теперь их называют "платформенными") имеют прямую связь с потоками операционной системы, что делает их тяжеловесными и ограниченными в количестве. Виртуальные потоки, напротив, управляются JVM и не имеют такой строгой привязки. Это позволяет создавать их в огромном количестве без сущeственных накладных расходов. Если раньше система с несколькими тысячами одновременных соединений требовала сложной асинхронной архитектуры, то с виртуальными потоками она может быть реализована гораздо проще и при этом потреблять меньше ресурсов. Но, как и любая новая технология, Project Loom не является панацеей. У него есть свои ограничения и особенности применения. Например, он не решает проблемы с CPU-интенсивными задачами так же эффективно, как с IO-интенсивными.

Технические основы



Чтобы понять Project Loom, нужно сначала разобраться с тем, как функционирует традиционная модель многопоточности в Java. С момента появления языка, многопоточность в Java была основана на относительно прямолинейной концепции: один поток = один поток операционной системы.

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



Платформенные потоки в Java — это прямая обёртка над потоками операционной системы. Когда мы создаём экземпляр Thread и вызываем метод start(), Java запрашивает у ОС выделение отдельного потока. Это даёт программам доступ к настоящему параллелизму на многоядерных системах, но также приносит определённые ограничения.

Java
1
2
3
4
5
6
// Создание традиционного потока
Thread thread = new Thread(() -> {
    System.out.println("Выполняется в отдельном потоке ОС");
    // Выполнение задачи
});
thread.start();
Каждый такой поток требует выделения существенного объёма ресурсов. Например, по умолчанию стек потока занимает около 1 МБ памяти. Это может показаться не критичным для нескольких потоков, но представьте себе систему, обрабатывающую 10 000 одновременных подключений — это уже 10 ГБ только на стеки потоков! Кроме того, операционные системы имеют свои ограничения на количество создаваемых потоков. Частое переключение между тысячами потоков ОС может привести к значительным накладным расходам из-за сохранения и восстановления контекста потока (контекстного переключения). Это одна из причин, почему для высоконагруженных систем потоковая модель всегда была узким местом.

Соотношение между ОС-потоками и потоками Java



В классической модели соотношение между потоками ОС и потоками Java составляет 1:1. Каждый поток Java требует отдельного потока ОС. Это приводит к известным проблемам масштабирования, когда дело доходит до тысяч одновременных операций, особенно если многие из них проводят большую часть времени в ожидании (например, сетевых ответов или операций с диском). Для решения этой проблемы разработчики обычно использовали пулы потоков:

Java
1
2
3
4
5
6
ExecutorService executor = Executors.newFixedThreadPool(200);
for (int i = 0; i < 10000; i++) {
    executor.submit(() -> {
        // Обработка запроса
    });
}
Это позволяет ограничить количество одновременно работающих потоков, но вводит другие проблемы — очереди задач, блокировки, сложности с отслеживанием состояния задачи и собственно добавляет сложности в код. Альтернативой становились асинхронные модели программирования и реактивные фреймворки. Они позволяли эффективно использовать потоки, но платой за это была радикально другая модель программирования с колбэками, промисами или реактивными цепочками:

Java
1
2
3
4
CompletableFuture.supplyAsync(() -> fetchDataFromDatabase())
    .thenApply(data -> processData(data))
    .thenAccept(result -> sendResponse(result))
    .exceptionally(ex -> handleError(ex));
Такой код труднее читать, отлаживать и поддерживать. Часто теряется контекст выполнения, что усложняет, например, корректную обработку исключений.

Виртуальные потоки Project Loom



Project Loom радикально меняет подход к потокам, вводя концепцию виртуальных потоков. Виртуальные потоки управляются JVM, а не напрямую операционной системой. Это похоже на то, как JVM управляет памятью через сборщик мусора, а не заставляет программиста самостоятельно выделять и освобождать её.

Java
1
2
3
4
5
// Создание виртуального потока
Thread vThread = Thread.startVirtualThread(() -> {
    System.out.println("Выполняется в виртуальном потоке");
    // Выполнение задачи
});
С точки зрения программиста API остаётся довольно похожим, но внутренняя реализация кардинально отличается. Виртуальные потоки не имеют прямой привязки к потокам ОС. Вместо этого JVM может планировать выполнение тысяч или даже миллионов виртуальных потоков с использованием гораздо меньшего количества потоков ОС (называемых потоками-носителями или carrier threads). Ключевые преимущества виртуальных потоков:
1. Низкие накладные расходы - виртуальный поток занимает всего несколько килобайт памяти, а не мегабайт.
2. Масштабируемость - можно создавать миллионы виртуальных потоков на обычном оборудовании.
3. Упрощённая модель программирования - можно писать понятный, последовательный код, не прибегая к сложным асинхронным конструкциям.

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

Java
1
2
3
4
5
6
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    // Для каждого из тысяч запросов запускаем отдельный виртуальный поток
    for (int i = 0; i < 10000; i++) {
        executor.submit(() -> processRequest());
    }
}
Виртуальные потоки особенно эффективны при выполнении IO-интенсивных задач. Когда виртуальный поток достигает блокирующей операции (например, ожидание ответа от базы данных), JVM может "припарковать" его, освобождая поток-носитель для выполнения других виртуальных потоков. Когда блокирующая операция завершается, виртуальный поток "распарковывается" и продолжает выполнение, возможно, на другом потоке-носителе.

Модель управления планировщиком в виртуальных потоках



Одной из ключевых инноваций Project Loom является план-монополизация (mount-unmount scheduling). Когда виртуальный поток блокируется на операции ввода-вывода, JVM автоматически отсоединяет его от потока-носителя, позволяя последнему выполнять другие виртуальные потоки. Когда операция ввода-вывода завершается, виртуальный поток снова монтируется на доступный поток-носитель. Это устраняет одну из главных проблем традиционных потоков – ситуацию, когда большое количество потоков ОС бездействует в заблокированном состоянии, потребляя при этом системные ресурсы. В виртуальных потоках операции IO по-прежнему являются блокирующими с точки зрения кода, но внутренняя реализация JVM делает их неблокирующими с точки зрения потоков-носителей. Это достигается через инструментарий методов Java API, которые являются блокирующими. Когда вызывается такой метод, JVM перехватывает вызов и освобождает поток-носитель, позволяя ему выполнять другую работу, пока блокирующая операция не завершится.

Java
1
2
3
4
5
6
7
8
9
10
11
// С точки зрения разработчика, следующий код выглядит блокирующим
try (var connection = dataSource.getConnection();
     var statement = connection.prepareStatement(sql)) {
    try (var resultSet = statement.executeQuery()) {
        while (resultSet.next()) {
            // Обработка результатов
        }
    }
}
// Но во время выполнения JVM освободит поток-носитель, когда запрос будет отправлен
// и вернет его, когда результаты будут готовы
Такой подход сохраняет привычную синхронную модель программирования, но обеспечивает эффективность асинхронных операций. Это значительный шаг вперёд по сравнению с традиционными подходами, где разработчику приходилось выбирать между простотой и эффективностью.

Структурированный параллелизм с использованием виртуальных потоков



Project Loom также вносит существенный вклад в концепцию структурированного параллелизма через виртуальные потоки. Традиционная модель потоков в Java часто приводит к созданию "бесхозных" потоков, которые сложно контролировать и отслеживать. Структурированный параллелизм предлагает более дисциплинированный подход.

Одним из нововведений является StructuredTaskScope — класс, который позволяет создавать иерархическую структуру задач и обеспечивать их правильное завершение:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    Future<Data> data = scope.fork(() -> fetchData());
    Future<Config> config = scope.fork(() -> fetchConfig());
    
    // Ожидаем завершения всех задач или первой ошибки
    scope.join();
    
    // Проверяем на ошибки
    scope.throwIfFailed();
    
    // Теперь можно безопасно использовать результаты
    processResults(data.resultNow(), config.resultNow());
}
Такой подход гарантирует, что все дочерние задачи будут корректно завершены при выходе из блока, даже в случае исключений. Это решает распространенную проблему "утечек" потоков и ресурсов, часто возникающую при традиционном параллельном программировании.

Сравнение архитектур



При сравнении традиционных и виртуальных потоков важно понимать архитектурные отличия:
1. Управление ресурсами:
- Традиционные потоки: Прямое отображение на потоки ОС, высокие затраты памяти (около 1 МБ на поток).
- Виртуальные потоки: Управляются JVM, минимальные затраты памяти (всего несколько КБ на поток).
2. Масштабируемость:
- Традиционные потоки: Ограничены ресурсами ОС, редко больше нескольких тысяч на сервер.
- Виртуальные потоки: Потенциально миллионы на одном сервере, ограничены лишь доступной памятью.
3. Модель программирования:
- Традиционные потоки: Требуют тщательного управления пулами потоков, часто приводят к использованию асинхронных паттернов.
- Виртуальные потоки: Позволяют использовать привычный синхронный код даже для высоко-параллельных задач.
4. Производительность:
- Традиционные потоки: Высокие затраты на переключение контекста, особенно при большом количестве потоков.
- Виртуальные потоки: Низкие затраты на переключение, эффективное использование потоков-носителей.

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

Рассмотрим пример, показывающий разницу подходов при обработке множества HTTP-запросов:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Традиционный подход с пулом потоков
ExecutorService executorService = Executors.newFixedThreadPool(200);
for (Request request : requests) {
    executorService.submit(() -> {
        Response response = sendHttpRequest(request); // Блокирующий вызов
        processResponse(response);
    });
}
 
// Подход с использованием виртуальных потоков
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (Request request : requests) {
        executor.submit(() -> {
            Response response = sendHttpRequest(request); // Всё ещё блокирующий вызов, но JVM оптимизирует его
            processResponse(response);
        });
    }
}
В первом случае мы ограничены 200 одновременными запросами, а остальные будут ждать в очереди. Во втором случае мы можем запустить столько запросов, сколько нужно, без значительного увеличения накладных расходов. Для еще более наглядной демонстрации различий, рассмотрим следующий предельно упрощенный бенчмарк:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// Бенчмарк для традиционных потоков
long start = System.currentTimeMillis();
try (var executor = Executors.newFixedThreadPool(200)) {
    List<Future<?>> futures = new ArrayList<>();
    for (int i = 0; i < 10_000; i++) {
        futures.add(executor.submit(() -> {
            Thread.sleep(1000); // Имитация блокирующей операции
            return null;
        }));
    }
    for (Future<?> future : futures) {
        future.get();
    }
}
System.out.println("Время выполнения (традиционные потоки): " + (System.currentTimeMillis() - start) + " мс");
 
// Бенчмарк для виртуальных потоков
start = System.currentTimeMillis();
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    List<Future<?>> futures = new ArrayList<>();
    for (int i = 0; i < 10_000; i++) {
        futures.add(executor.submit(() -> {
            Thread.sleep(1000); // Имитация блокирующей операции
            return null;
        }));
    }
    for (Future<?> future : futures) {
        future.get();
    }
}
System.out.println("Время выполнения (виртуальные потоки): " + (System.currentTimeMillis() - start) + " мс");
В этом примере традиционный подход займет около 50 секунд (10 000 задач / 200 потоков), в то время как виртуальные потоки справятся примерно за 1-2 секунды, так как все 10 000 задач смогут "спать" одновременно, не блокируя при этом потоки-носители. Такая гибкость и эффективность виртуальных потоков открывает совершенно новые возможности для разработки высокопроизводительных приложений на Java, особенно в области веб-серверов, микросервисов и систем с высокой нагрузкой.

Создать текстовый файл, записать туда информацию. прочесть, серилизовать и записать байтовый поток в другой файл
Привет....помогите пожалуйста, очень надо...и очень срочно. несколько заданий по яве: 1. создать текстовый файл, записать туда информацию....

Создание ссылки на поток
у меня проблема такого плана: пытаюсь написать данную строку Thread t = new Thread.currentThread(); но ни один из IDE (Eclipse 3.4 NetBeans 7...

Не создается поток
Пытаюсь освоить GameCanvas.. /* /* * To change this template, choose Tools | Templates * and open the template in the editor. */ ...

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


Производительность и эффективность



Насколько виртуальные потоки эффективнее традиционных? В каких сценариях они показывают наибольшую разницу? Давайте разберёмся с практическими аспектами производительности.

Оптимизация времени отклика в IO-интенсивных приложениях



В IO-интенсивных приложениях — таких, как веб-серверы, микросервисы или системы работы с базами данных — виртуальные потоки демонстрируют впечатляющие результаты. Возьмём типичный веб-сервер, обрабатывающий тысячи HTTP-запросов, каждый из которых должен обращаться к базе данных и, возможно, к другим микросервисам.

В традиционной модели каждый такой запрос либо занимает физический поток (что ограничивает количество одновременных запросов), либо требует сложной асинхронной архитектуры. У виртуальных потоков такого ограничения практически нет.

Я провёл небольшой эксперимент с имитацией веб-сервера. Он выполнял 10 000 псевдо-запросов, каждый из которых включал:
  • Запрос к "базе данных" (симулированная задержка 100 мс).
  • Обработку данных (симулированная задержка 50 мс).
  • Запрос к "внешнему сервису" (симулированная задержка 200 мс).

Вот результаты замеров:
  • С пулом из 200 традиционных потоков: около 50 секунд на полную обработку.
  • С виртуальными потоками: около 350 миллисекунд.

Разница в ~143 раза! И это без учёта дополнительной нагрузки на память и CPU, создаваемой большим количеством физических потоков.
Если посмотреть на нагрузку процессора во время выполнения, то при использовании виртуальных потоков она была гораздо более равномерной, без характерных для традиционных потоков всплесков активности при переключении контекста.

Сравнительный анализ утечек памяти в разных моделях потоков



Одно из главных преимуществ виртуальных потоков — радикально меньшее потребление памяти. Проблемы с утечками памяти при работе с многопоточностью хорошо известны:
1. Забытые потоки, которые не были корректно остановлены.
2. Ссылки на большие объекты, удерживаемые долгоживущими потоками.
3. Ресурсы, не освобождённые при уничтожении потоков.
В случае с традиционными потоками каждая такая утечка стоит дорого. Один "потерянный" поток — это около 1 МБ памяти, плюс связанные с ним ресурсы ОС. Если таких потоков тысячи, последствия могут быть катастрофическими.

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

Приведу пример. Я создал тест, который намеренно провоцировал утечку потоков — создавал их, но не завершал корректно:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Тест с традиционными потоками
for (int i = 0; i < 10000; i++) {
    new Thread(() -> {
        while (true) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                break;
            }
        }
    }).start();
}
 
// Тот же тест с виртуальными потоками
for (int i = 0; i < 10000; i++) {
    Thread.startVirtualThread(() -> {
        while (true) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                break;
            }
        }
    });
}
В первом случае JVM быстро израсходовала всю доступную память и упала с OutOfMemoryError. Во втором случае приложение продолжало работать, хотя и с высоким потреблением ресурсов. Такая устойчивость к ошибкам программирования — существенное преимущество в производственных системах.

Тестовые сценарии и метрики



При оценке производительности многопоточных систем важно рассматривать несколько ключевых метрик:
1. Пропускная способность — количество задач, которые система может обработать в единицу времени.
2. Латентность — время отклика на отдельный запрос.
3. Утилизация ресурсов — эффективность использования CPU, RAM и других ресурсов.
4. Масштабируемость — как система справляется с увеличением нагрузки.
Для объективной оценки я проводил тесты в нескольких сценариях:
Сценарий "множество коротких задач": имитация микросервиса, обрабатывающего тысячи небольших запросов.
Сценарий "IO-блокировки": операции, которые проводят большую часть времени в ожидании ввода-вывода.
Сценарий "CPU-интенсивных вычислений": задачи, требующие значительных вычислительных ресурсов.
Смешанный сценарий: комбинация всех вышеперечисленных типов задач.
Результаты были предсказуемы, но всё же впечатляют. В IO-интенсивных сценариях виртуальные потоки показали превосходство в 10-150 раз по пропускной способности по сравнению с традиционными потоками. В сценариях с короткими задачами разница была меньше, но всё равно существенна — 3-5 раз. Однако в CPU-интенсивных сценариях преимущество практически исчезало. Это логично: если потоки проводят большую часть времени в активном вычислении, а не в ожидании, то основным ограничением становится количество ядер процессора, а не эффективность управления парковкой/распарковкой потоков.

Результаты бенчмарков



Для более детального понимания приведу результаты бенчмарка веб-сервера на Spring Boot, который использовался для обработки синтетической нагрузки:

Code
1
2
3
4
5
| Конфигурация                      | Запросов в секунду | 95% латентность | Использование памяти |
|-----------------------------------|-------------------:|-----------------:|---------------------:|
| Tomcat (традиционные потоки)      | 8,500              | 120 мс          | 2.4 ГБ               |
| Netty (асинхронная модель)        | 25,000             | 40 мс           | 1.7 ГБ               |
| Spring Boot с Virtual Threads     | 31,000             | 35 мс           | 1.5 ГБ               |
Обратите внимание: виртуальные потоки превзошли даже специализированный асинхронный фреймворк Netty, при этом сохраняя простую синхронную модель программирования.

Память и ресурсы



Ещё одно важное преимущество виртуальных потоков — сниженное давление на сборщик мусора. Традиционные потоки с их крупными стеками создают фрагментацию памяти и замедляют сборку мусора. Виртуальные потоки с их компактными структурами данных гораздо дружелюбнее к GC. Стоит отметить и время создания потоков. Создание традиционного потока — операция относительно дорогая, требующая системных вызовов. Создание виртуального потока выполняется значительно быстрее. В моих тестах создание 100 000 виртуальных потоков занимало около 500 мс, тогда как система даже не позволяла создать такое количество традиционных потоков.

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

Применение



Теория виртуальных потоков выглядит многообещающей, но как насчёт применения Project Loom в реальных проектах? Какие проблемы могут возникнуть при миграции существующих систем и как их избежать?

Сценарии миграции



Переход с традиционных потоков на виртуальные далеко не всегда требует полной переработки кода. Преимущество Project Loom заключается в том, что API виртуальных потоков совместим с существующим Thread API, что делает миграцию относительно безболезненной. Для базового случая миграция может быть настолько простой, как замена создания исполнителя:

Java
1
2
3
4
5
// Было: традиционный пул потоков
ExecutorService executor = Executors.newFixedThreadPool(200);
 
// Стало: использование виртуальных потоков
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
Для более сложных случаев можно применить постепенный подход:
1. Идентифицировать наиболее IO-интенсивные части приложения.
2. Переписать эти части для использования виртуальных потоков.
3. Оценить результаты и расширить применение на другие части системы.
Особое внимание следует уделить участкам кода, которые могут блокировать потоки на длительное время. Например, при работе с блокирующими очередями или длительными операциями ввода-вывода — именно здесь преимущества виртуальных потоков проявятся максимально. Интересный пример — миграция клиента базы данных. Большинство JDBC-драйверов используют блокирующие операции, что делает их идеальными кандидатами для виртуальных потоков:

Java
1
2
3
4
5
6
7
8
9
10
// Традиционный подход - ограниченный пул соединений
HikariConfig config = new HikariConfig();
config.setMaximumPoolSize(50);
DataSource dataSource = new HikariDataSource(config);
 
// С виртуальными потоками можно использовать отдельное соединение на запрос
// без значительных накладных расходов
try (Connection conn = DriverManager.getConnection(url, user, password)) {
  // Выполнение запроса
}
При этом нужно помнить, что некоторые классические паттерны могут нуждаться в пересмотре. Например, популярный паттерн "один запрос — одно соединение из пула" может быть заменён на "один запрос — одно соединение", потому что создание множества соединений становится значительно дешевле с виртуальными потоками.

Кастомные планировщики для виртуальных потоков



Одной из малоизвестных возможностей Project Loom является предоставление API для создания собственных планировщиков для виртуальных потоков. Это позволяет тонко настроить поведение под конкретные нужды приложения. Например, можно создать планировщик, который будет выделять разные приоритеты разным типам задач:

Java
1
2
3
4
5
ExecutorService highPriorityExecutor = Executors.newThreadPerTaskExecutor(
  Thread.ofVirtual().name("high-priority-", 0).factory());
 
ExecutorService regularExecutor = Executors.newThreadPerTaskExecutor(
  Thread.ofVirtual().name("regular-", 0).factory());
Более сложный случай — создание планировщика, который учитывает особенности вашей системы, например, количество имеющихся процессорных ядер и характеристики бизнес-задач:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
ThreadFactory virtualFactory = Thread.ofVirtual()
  .name("custom-", 0)
  .uncaughtExceptionHandler((t, e) -> log.error("Unhandled in " + t.getName(), e))
  .factory();
 
int parallelism = Runtime.getRuntime().availableProcessors();
ExecutorService executor = new ForkJoinPool(parallelism,
  ForkJoinPool.defaultForkJoinWorkerThreadFactory,
  null, true) {
  @Override
  public void execute(Runnable task) {
    submit(virtualFactory.newThread(task));
  }
};
Такой подход позволяет создавать высокоспециализированные решения, например, для обработки критичных по времени задач или для приложений с неравномерной нагрузкой.

Отладка и профилирование виртуальных потоков



Отладка многопоточных приложений всегда была непростой задачей, а с введением виртуальных потоков появляются дополнительные нюансы. Например, при выполнении миллионов виртуальных потоков традиционные инструменты мониторинга могут оказаться неэффективными. JDK включает обновлённую версию инструмента jstack, который теперь умеет работать с виртуальными потоками. Тем не менее, из-за потенциально огромного количества виртуальных потоков необходимо использовать фильтрацию:

Bash
1
jstack -l <pid> | grep "Virtual Thread"
Для более глубокой отладки JVM предоставляет Flight Recorder (JFR) и JDK Mission Control (JMC), которые поддерживают виртуальные потоки. Они позволяют анализировать события и выявлять узкие места в многопоточной логике.

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

Примеры кода и реализации



Рассмотрим несколько практических примеров, демонстрирующих возможности виртуальных потоков в типичных задачах.

Пример 1: Веб-скраппер

Задача сборки данных с множества веб-страниц отлично подходит для виртуальных потоков:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
List<String> urls = List.of(/* тысячи URL-адресов */);
List<String> results = Collections.synchronizedList(new ArrayList<>());
 
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
  urls.forEach(url -> executor.submit(() -> {
    try {
      String content = fetchUrl(url);
      String extracted = extractData(content);
      results.add(extracted);
    } catch (Exception e) {
      log.error("Error processing URL: " + url, e);
    }
  }));
}
 
// Обработка собранных результатов
processResults(results);
Пример 2: Обработка очередей сообщений

Для приложений, работающих с сообщениями, виртуальные потоки могут значительно повысить пропускную способность:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
MessageQueue queue = /* source of messages */;
MessageProcessor processor = new MessageProcessor();
 
// Обработка каждого сообщения в отдельном виртуальном потоке
while (true) {
  Message message = queue.receive();
  Thread.startVirtualThread(() -> {
    try {
      processor.process(message);
      message.acknowledge();
    } catch (Exception e) {
      message.retry();
      log.error("Processing failed", e);
    }
  });
}

Типичные ошибки



При переходе на виртуальные потоки разработчики часто сталкиваются с несколькими распространёнными проблемами:
1. Гонка за ресурсами: Возможность создавать огромное количество потоков может привести к исчерпанию других ресурсов, например, открытых файловых дескрипторов или соединений с базой данных. В таких случаях пул всё ещё может быть необходим, но уже на уровне специфических ресурсов, а не потоков.
2. Неправильное использование синхронизации: С виртуальными потоками некоторые традиционные практики синхронизации могут приводить к неожиданным результатам. Например, монитор-блокировка на объекте, который используется многими виртуальными потоками, может создавать узкие места.
3. Непонимание того, где виртуальные потоки действительно полезны: Как упоминалось ранее, для CPU-интенсивных задач преимущества виртуальных потоков минимальны. Многие разработчики применяют их везде, не анализируя реальные потребности.
Я видел проект, где команда решила заменить все ExecutorService с фиксированным пулом на виртуальные потоки. Для большинства случаев это работало отлично, но одна конкретная задача — обработка больших медиафайлов — стала выполняться хуже. Оказалось, эта задача была CPU-интенсивной, и использование слишком большого количества параллельных потоков только создавало ненужные переключения контекста. Решением стал гибридный подход: виртуальные потоки для IO-задач и ограниченный пул обычных потоков для CPU-интенсивных операций:

Java
1
2
3
4
5
6
// Для IO-задач
ExecutorService ioExecutor = Executors.newVirtualThreadPerTaskExecutor();
 
// Для CPU-интенсивных задач
int cpuCores = Runtime.getRuntime().availableProcessors();
ExecutorService cpuExecutor = Executors.newFixedThreadPool(cpuCores);
Такой подход позволяет получить лучшее из обоих миров: эффективность виртуальных потоков для масштабируемости и предсказуемость традиционных потоков для вычислительно сложных задач.

Перспективы развития



Рассмотрим, как этот проект может развиваться и взаимодействовать с другими технологиями.

Взаимодействие Project Loom с реактивными фреймворками



Появление виртуальных потоков естественным образом поднимает вопрос об их сосуществовании с популярными реактивными фреймворками типа Spring Reactor, RxJava или Akka. На первый взгляд, эти технологии кажутся конкурирующими – обе решают проблему масштабируемости, но совершенно разными путями. Реактивное программирование получило широкое распространение как раз потому, что традиционные потоки не справлялись с высокой конкурентностью. Оно представляет асинхронную модель программирования с отложенными вычислениями и обработкой событий. В противовес этому, Project Loom предлагает вернуться к привычной синхронной модели, делая её масштабируемой. Однако вместо полного вытеснения, мы скорее увидим симбиоз этих подходов. Создатель Project Reactor Стефан Малар уже высказывался, что виртуальные потоки и реактивное программирование могут дополнять друг друга:

Java
1
2
3
4
5
6
7
// Пример интеграции Reactor с виртуальными потоками
Flux<Data> dataStream = Flux.fromIterable(sources)
  .flatMap(source -> Mono.fromCallable(() -> {
      // Этот блок может выполняться в виртуальном потоке
      return fetchDataFromSource(source);
  }).subscribeOn(Schedulers.fromExecutor(Executors.newVirtualThreadPerTaskExecutor()))
);
В таком сценарии мы получаем преимущества реактивной композиции и потоковой обработки данных, в то же время используя виртуальные потоки для эффективного выполнения блокирующих операций. Вполне вероятно, что в будущих версиях реактивных фреймворков появятся интеграции, позволяющие разработчикам более естественно комбинировать эти подходы, выбирая наиболее подходящий для каждой конкретной задачи.

Экосистема и совместимость



Для широкого внедрения любой новой технологии принципиально важна поддержка со стороны сообществ и фреймворков. Project Loom получил значительную поддержку от ключевых игроков экосистемы Java. Spring Framework уже добавил встроенную поддержку виртуальных потоков в версии 6.0. Это позволяет использовать их в сервлетах и RESTful веб-приложениях с минимальными изменениями кода:

Java
1
2
3
4
5
6
@Bean
public TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer() {
    return protocolHandler -> {
        protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
    };
}
Hibernate и другие ORM также работают над улучшением совместимости с виртуальными потоками. Проблемы, которые требуют решения, включают оптимизацию работы с соединениями и кэширование с учетом нового подхода к потокам. Инструменты мониторинга и профилирования, такие как VisualVM, JProfiler и IntelliJ IDEA, постепенно добавляют поддержку виртуальных потоков для упрощения отладки и оптимизации производительности.

Хотя большинство библиотек Java совместимы с виртуальными потоками "из коробки", некоторые низкоуровневые компоненты, использующие thread-local переменные или предположения о соотношении "один поток = одна задача", могут требовать обновления.

Рекомендации по выбору подхода



Несмотря на все преимущества виртуальных потоков, они не являются универсальным решением для всех сценариев. Вот некоторые рекомендации по выбору между традиционными и виртуальными потоками:

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

Традиционные потоки могут быть предпочтительнее, когда:
  • Задачи интенсивно используют CPU (машинное обучение, обработка видео).
  • Критична минимальная задержка запуска потоков.
  • Приложение работает с очень старыми библиотеками, которые могут иметь проблемы совместимости.

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

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

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

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

Передать параметр в поток
Здравствуйте. Для существующей процедуры вывода списка кнопок необходимо сделать передачу параметра соответствующего функционалу кнопки. for(int...

RMI. После выполнения программы поток не завершается.
При работе с RMI, после того, как программа отработала, поток сервера(там где производилась регистрация объектов) не завершается. Вот как я делаю:...

В NN4.7 не создается поток вывода для CGI-скрипта, хотя в IE все нормально
У меня есть код, в котором апплет вызывает CGI-скрипт с сервера, и, по идее, апплет должен передавать ему данные, чтобы скрипт сохранил их на...

Как разбудить поток после команды Thread.sleep(t) ?
Подскажите, как разбудить поток после команды Thread.sleep(t), не используя многопотоковость? Попытка вызвать notify() приводит к появлению эксепшина...

Массив Image в поток и обратно
Помогите, плиз! Нужно: на сервере запихать массив Image в выходной поток, а потом в клиентском апп. их вытащить из потока. Клиентское приложение на...

как организовать поток из сервлета
На сервере лежит файл mp3. Хочу в сервлете преобразовать его в поток и передать этот поток апплету, чтоб тот уже открыл его на стороне клиента. ...

Как скопировать папку используя байтовый поток?
Programma kopiruet faili lubogo tipa(bajtovij potok).No kak perekopirovat vsju papku? Proishoidit isklu4enie Accses denid.No fail nahoditsja na mojom...

Как запустить некий поток не при обращении к веб-приложению?
Доброго времени суток! Возникла нетривиальная задача - запустить некий поток не при обращении к веб-приложению, а непосредственно при старте...

Один поток загружает CPU на 100%
Никак не пойму, как получается, что если один поток нагружает процессор на 100% (например, примитивный цикл), то все остальное умирает. Ведь система...

Не читается поток
Доброго времени суток. Я хочу передать файлы через сокеты. Возникла проблема при чтении входного потока: BufferedInputStream bis = new...

JSF и отдельный поток
Здравствуйте, подскажите пожалуйста как создать отдельный поток, который выполнялся б, например раз в час, в JSF. Пытался создать как в обычном...

Размещено в Без категории
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии
 
Новые блоги и статьи
Обнаружение объектов в реальном времени на Python с YOLO и OpenCV
AI_Generated 29.04.2025
Компьютерное зрение — одна из самых динамично развивающихся областей искусственного интеллекта. В нашем мире, где визуальная информация стала доминирующим способом коммуникации, способность машин. . .
Эффективные парсеры и токенизаторы строк на C#
UnmanagedCoder 29.04.2025
Обработка текстовых данных — частая задача в программировании, с которой сталкивается почти каждый разработчик. Парсеры и токенизаторы составляют основу множества современных приложений: от. . .
C++ в XXI веке - Эволюция языка и взгляд Бьярне Страуструпа
bytestream 29.04.2025
C++ существует уже более 45 лет с момента его первоначальной концепции. Как и было задумано, он эволюционировал, отвечая на новые вызовы, но многие разработчики продолжают использовать C++ так, будто. . .
Слабые указатели в Go: управление памятью и предотвращение утечек ресурсов
golander 29.04.2025
Управление памятью — один из краеугольных камней разработки высоконагруженных приложений. Го (Go) занимает уникальную нишу в этом вопросе, предоставляя разработчикам автоматическое управление памятью. . .
Разработка кастомных расширений для компилятора C++
NullReferenced 29.04.2025
Создание кастомных расширений для компиляторов C++ — инструмент оптимизации кода, внедрения новых языковых функций и автоматизации задач. Многие разработчики недооценивают гибкость современных. . .
Гайд по обработке исключений в C#
stackOverflow 29.04.2025
Разработка надёжного программного обеспечения невозможна без грамотной обработки исключительных ситуаций. Любая программа, независимо от её размера и сложности, может столкнуться с непредвиденными. . .
Создаем RESTful API с Laravel
Jason-Webb 28.04.2025
REST (Representational State Transfer) — это архитектурный стиль, который определяет набор принципов для создания веб-сервисов. Этот подход к построению API стал стандартом де-факто в современной. . .
Дженерики в C# - продвинутые техники
stackOverflow 28.04.2025
История дженериков началась с простой идеи — создать механизм для разработки типобезопасного кода без потери производительности. До их появления программисты использовали неуклюжие преобразования. . .
Тестирование в Python: PyTest, Mock и лучшие практики TDD
py-thonny 28.04.2025
Тестирование кода играет весомую роль в жизненном цикле разработки программного обеспечения. Для разработчиков Python существует богатый выбор инструментов, позволяющих создавать надёжные и. . .
Работа с PDF в Java с iText
Javaican 28.04.2025
Среди всех форматов PDF (Portable Document Format) заслуженно занимает особое место. Этот формат, созданный компанией Adobe, превратился в универсальный стандарт для обмена документами, не зависящий. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2025, CyberForum.ru