Почему создание потока считается дорогостоящим?
В руководствах по Java говорится, что создание потока обходится дорого. Но почему именно это дорого? Что именно происходит при создании потока Java, что делает его создание дорогостоящим? Я принимаю утверждение за истину, но меня просто интересует механика создания потока в JVM.
Накладные расходы на жизненный цикл потока. Создание и демонтаж потока не бесплатны. Фактические накладные расходы различаются на разных платформах, но создание потока требует времени, что приводит к задержке при обработке запросов, и требует некоторой обработки со стороны JVM и операционной системы. Если запросы частые и легкие, как в большинстве серверных приложений, создание нового потока для каждого запроса может потребовать значительных вычислительных ресурсов.
Из Параллелизм Java на практике
Брайан Гетц, Тим Пайерлс, Джошуа Блох, Джозеф Боубир, Дэвид Холмс, Дуг Ли
ISBN для печати-10: 0-321-34960-1
Переведено автоматически
Ответ 1
Почему создание потока считается дорогостоящим?
Потому что это >><< дорого.
Создание потока на Java обходится дорого, потому что требует изрядного объема работы:
- Для стека потоков необходимо выделить и инициализировать большой блок памяти.
- Для создания / регистрации собственного потока в операционной системе хоста необходимо выполнить системные вызовы.
- Дескрипторы необходимо создавать, инициализировать и добавлять во внутренние структуры данных JVM.
Это также дорого в том смысле, что поток связывает ресурсы до тех пор, пока он активен; например, стек потоков, любые объекты, доступные из стека, дескрипторы потоков JVM, дескрипторы собственных потоков ОС.
Затраты на все эти вещи зависят от платформы, но они недешевы ни на одной платформе Java, с которой я когда-либо сталкивался.
Поиск в Google нашел старый бенчмарк, который сообщает о скорости создания потоков ~ 4000 в секунду на Sun Java 1.4.1 на двухпроцессорном Xeon 2002 года выпуска под управлением 2002 года выпуска Linux. Более современная платформа даст лучшие показатели ... и я не могу комментировать методологию ... но, по крайней мере, это дает приблизительное представление о том, насколько дорогим может быть создание потока.
Сравнительный анализ Питера Лоури показывает, что в абсолютном выражении создание потока в наши дни происходит значительно быстрее, но неясно, насколько это связано с улучшениями в Java и / или операционной системе ... или с более высокими скоростями процессора. Но его цифры все еще указывают на улучшение более чем в 150 раз, если вы используете пул потоков по сравнению с созданием / запуском нового потока каждый раз. (И он подчеркивает, что все это относительно ...)
Вышесказанное предполагает собственные потоки, а не "зеленые потоки", но все современные JVM используют собственные потоки по соображениям производительности. "Зеленые потоки", возможно, дешевле создавать, но вы платите за это в других областях.
Обновление: Проект Loom OpenJDK направлен, среди прочего, на предоставление облегченной альтернативы стандартным потокам Java. Они предлагают виртуальные потоки, которые представляют собой гибрид собственных потоков и зеленых потоков. Проще говоря, виртуальный поток скорее похож на реализацию green thread, которая использует собственные потоки под ним, когда требуется параллельное выполнение.
На данный момент (июль 2023 г.) Project Loom стал JEP 444. Он находится в предварительном просмотре с Java 19 и предлагается к полному выпуску в Java 21.
Я немного покопался, чтобы увидеть, как на самом деле распределяется стек потока Java. В случае OpenJDK 6 в Linux стек потоков выделяется вызовом pthread_create
, который создает собственный поток. (JVM не передает pthread_create
предварительно выделенный стек.)
Затем внутри pthread_create
стек распределяется вызовом mmap
следующим образом:
mmap(0, attr.__stacksize,
PROT_READ|PROT_WRITE|PROT_EXEC,
MAP_PRIVATE|MAP_ANONYMOUS, -1, 0)
Согласно man mmap
, флаг MAP_ANONYMOUS
приводит к инициализации памяти равным нулю.
Таким образом, хотя может быть и не обязательно, чтобы новые стеки потоков Java были обнулены (согласно спецификации JVM), на практике (по крайней мере, с OpenJDK 6 в Linux) они обнуляются.
Ответ 2
Другие обсуждали, откуда берутся затраты на потоковую обработку. Этот ответ объясняет, почему создание потока не такое уж дорогое занятие по сравнению со многими операциями, но относительно дорогое занятие по сравнению с альтернативами выполнения задач, которые относительно дешевле.
Наиболее очевидной альтернативой выполнению задачи в другом потоке является выполнение задачи в том же потоке. Это трудно понять тем, кто полагает, что чем больше потоков, тем всегда лучше. Логика заключается в том, что если накладные расходы на добавление задачи в другой поток больше, чем время, которое вы экономите, выполнение задачи в текущем потоке может быть быстрее.
Другой альтернативой является использование пула потоков. Пул потоков может быть более эффективным по двум причинам. 1) он повторно использует уже созданные потоки. 2) вы можете настраивать / контролировать количество потоков, чтобы обеспечить оптимальную производительность.
Печатается следующая программа....
Time for a task to complete in a new Thread 71.3 us
Time for a task to complete in a thread pool 0.39 us
Time for a task to complete in the same thread 0.08 us
Time for a task to complete in a new Thread 65.4 us
Time for a task to complete in a thread pool 0.37 us
Time for a task to complete in the same thread 0.08 us
Time for a task to complete in a new Thread 61.4 us
Time for a task to complete in a thread pool 0.38 us
Time for a task to complete in the same thread 0.08 us
Это тест для тривиальной задачи, которая увеличивает накладные расходы на каждый параметр потоковой обработки. (Эта тестовая задача относится к тому типу задач, которые на самом деле лучше всего выполняются в текущем потоке.)
final BlockingQueue<Integer> queue = new LinkedBlockingQueue<Integer>();
Runnable task = new Runnable() {
@Override
public void run() {
queue.add(1);
}
};
for (int t = 0; t < 3; t++) {
{
long start = System.nanoTime();
int runs = 20000;
for (int i = 0; i < runs; i++)
new Thread(task).start();
for (int i = 0; i < runs; i++)
queue.take();
long time = System.nanoTime() - start;
System.out.printf("Time for a task to complete in a new Thread %.1f us%n", time / runs / 1000.0);
}
{
int threads = Runtime.getRuntime().availableProcessors();
ExecutorService es = Executors.newFixedThreadPool(threads);
long start = System.nanoTime();
int runs = 200000;
for (int i = 0; i < runs; i++)
es.execute(task);
for (int i = 0; i < runs; i++)
queue.take();
long time = System.nanoTime() - start;
System.out.printf("Time for a task to complete in a thread pool %.2f us%n", time / runs / 1000.0);
es.shutdown();
}
{
long start = System.nanoTime();
int runs = 200000;
for (int i = 0; i < runs; i++)
task.run();
for (int i = 0; i < runs; i++)
queue.take();
long time = System.nanoTime() - start;
System.out.printf("Time for a task to complete in the same thread %.2f us%n", time / runs / 1000.0);
}
}
}
Как вы можете видеть, создание нового потока занимает всего ~ 70 мкс. Это можно считать тривиальным во многих, если не в большинстве случаев использования. Условно говоря, это дороже, чем альтернативы, и для некоторых ситуаций пул потоков или вообще не использование потоков является лучшим решением.
Ответ 3
Теоретически это зависит от JVM. На практике каждый поток имеет относительно большой объем стековой памяти (по умолчанию, я думаю, 256 КБ). Кроме того, потоки реализованы как потоки операционной системы, поэтому для их создания требуется вызов операционной системы, то есть переключение контекста.
Вы понимаете, что "дорого" в вычислениях всегда очень относительно. Создание потока очень дорого по сравнению с созданием большинства объектов, но не очень дорого по сравнению со случайным поиском на жестком диске. Вам не нужно избегать создания потоков любой ценой, но создавать сотни потоков в секунду - не самый разумный шаг. В большинстве случаев, если ваш дизайн требует большого количества потоков, вам следует использовать пул потоков ограниченного размера.
Ответ 4
Существует два вида потоков:
Правильные потоки: это абстракции, связанные со средствами потоковой обработки базовой операционной системы. Следовательно, создание потока так же дорого, как и системы - всегда есть накладные расходы.
"Зеленые" потоки: создаваемые и планируемые JVM, они дешевле, но надлежащего параллелизма не происходит. Они ведут себя как потоки, но выполняются в потоке JVM в операционной системе. Насколько мне известно, они используются нечасто.
Самый большой фактор, о котором я могу подумать при затратах на создание потока, - это размер стека, который вы определили для своих потоков. Размер стека потока может быть передан в качестве параметра при запуске виртуальной машины.
Кроме этого, создание потока в основном зависит от операционной системы и даже от реализации виртуальной машины.
Теперь позвольте мне кое-что уточнить: создание потоков обходится дорого, если вы планируете запускать 2000 потоков в секунду, каждую секунду вашего времени выполнения. JVM не предназначена для этого. Если у вас будет пара стабильных воркеров, которых не будут увольнять и убивать снова и снова, расслабьтесь.