Почему создание темы считается дорогой?

180

В руководствах по Java написано, что создание потока стоит дорого. Но почему именно это дорого? Что именно происходит, когда создается поток Java, что делает его создание дорогим? Я принимаю это утверждение как правду, но мне просто интересна механика создания потоков в JVM.

Поток жизненного цикла потока. Создание и удаление потоков не бесплатны. Фактические издержки варьируются в зависимости от платформы, но создание потока требует времени, внося задержку в обработку запроса и требуя некоторой активности обработки JVM и ОС. Если запросы частые и легкие, как в большинстве серверных приложений, создание нового потока для каждого запроса может потребовать значительных вычислительных ресурсов.

От параллельной практики Java на практике
Брайан Гетц, Тим Пайерлс, Джошуа Блох, Джозеф Боубер, Дэвид Холмс, Даг Ли
Печать ISBN-10: 0-321-34960-1

Качанов
источник
Я не знаю, в каком контексте прочитанные вами учебники говорят так: подразумевают ли они, что создание само по себе дорого, или что «создание потока» стоит дорого. Разница, которую я пытаюсь показать, заключается в чистом действии создания потока (давайте назовем его экземпляром или чем-то еще) или фактом, что у вас есть поток (так что использование потока: очевидно, что накладные расходы). Какой из них заявлен // о каком вы хотите спросить?
Нанн
9
@typoknig - Дорого по сравнению с НЕ создавая новую тему :)
willcodejavaforfood
возможно дублирование накладных расходов на создание потоков Java
Пол Дрейпер,
1
Нитки для победы. Не нужно всегда создавать новые темы для задач.
Александр Миллс

Ответы:

149

Создание потока Java стоит дорого, потому что в нем задействовано немало работы:

  • Большой блок памяти должен быть выделен и инициализирован для стека потоков.
  • Системные вызовы должны быть сделаны для создания / регистрации собственного потока с хост-ОС.
  • Дескрипторы должны быть созданы, инициализированы и добавлены во внутренние структуры данных JVM.

Это также дорого в том смысле, что поток связывает ресурсы, пока он жив; например, стек потоков, любые объекты, достижимые из стека, дескрипторы потоков JVM, дескрипторы собственных потоков ОС.

Стоимость всех этих вещей зависит от платформы, но они недешевы для любой платформы Java, с которой я когда-либо сталкивался.


Поиск Google нашел мне старый тест, который показывает скорость создания потоков ~ 4000 в секунду на Sun Java 1.4.1 на двухпроцессорном старом процессоре 2002 года Xeon под управлением винтажного Linux 2002 года. Более современная платформа даст лучшие цифры ... и я не могу комментировать методологию ... но, по крайней мере, она дает оценку того, насколько дорогостоящим будет создание потоков.

Бенчмаркинг Питера Лоури показывает, что в наши дни создание потоков значительно быстрее в абсолютном выражении, но неясно, насколько это связано с улучшениями в Java и / или ОС ... или более высокими скоростями процессора. Но его числа по- прежнему указывают на 150-кратное улучшение, если вы используете пул потоков вместо создания / запуска нового потока каждый раз. (И он подчеркивает, что это все относительно ...)


(Выше предполагается, что «собственные потоки», а не «зеленые потоки», но все современные JVM используют собственные потоки из соображений производительности. Создание зеленых потоков возможно дешевле, но вы платите за это в других областях.)


Я немного покопался, чтобы увидеть, как действительно выделяется стек потока 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
@Raedwald - это часть инициализации, которая стоит дорого. Где-то что-то (например, GC или ОС) будет обнулять байты до того, как блок будет превращен в стек потоков. Это занимает циклы физической памяти на типичном оборудовании.
Стивен С.
2
«Где-то что-то (например, GC или ОС) обнулит байты». Так и будет? ОС будет, если это требует выделения новой страницы памяти, из соображений безопасности. Но это будет необычно. И ОС может хранить кеш уже нулевых страниц (IIRC, Linux делает это). Зачем GC беспокоиться, учитывая, что JVM будет препятствовать любой программе Java читать ее содержимое? Обратите внимание, что стандартная malloc()функция C , которую JVM вполне может использовать, не гарантирует, что выделенная память обнуляется (предположительно, чтобы избежать именно таких проблем с производительностью).
Raedwald
1
stackoverflow.com/questions/2117072/… согласен с тем, что «одним из основных факторов является память стека, выделенная для каждого потока».
Raedwald
2
@Raedwald - см. Обновленный ответ для получения информации о том, как фактически распределяется стек.
Стивен С.
2
Вполне возможно (вероятно , даже) , что страницы памяти , выделенные mmap()вызова от копирования при записи отображается на нулевой странице, поэтому их initailisation происходит не в mmap()себе, но когда страницы сначала записываются на, а затем только одна страница в время. То есть, когда поток начинает выполнение, с расходами, созданными потоком, а не потоком-создателем.
Raedwald
76

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

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

Другой альтернативой является использование пула потоков. Пул потоков может быть более эффективным по двум причинам. 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 мкс. Это можно считать тривиальным во многих, если не в большинстве случаев использования. Условно говоря, это дороже, чем альтернативы, и для некоторых ситуаций лучшим решением является пул потоков или вообще не использующий потоки.

Питер Лори
источник
8
Это отличный кусок кода. Лаконично, к делу и четко отображает свой трюк.
Николай
Я полагаю, что в последнем блоке результат искажен, потому что в первых двух блоках основной поток удаляется параллельно, как рабочие потоки. Однако в последнем блоке действие взятия выполняется последовательно, поэтому оно расширяет значение. Возможно, вы могли бы использовать queue.clear () и использовать CountDownLatch вместо этого, чтобы дождаться завершения потоков.
Виктор Граци
@VictorGrazi Я предполагаю, что вы хотите собирать результаты централизованно. В каждом случае выполняется одинаковое количество очередей. Обратный отсчет будет немного быстрее.
Питер Лори
На самом деле, почему бы просто не сделать что-то постоянно быстрое, например, увеличить счетчик; отбросьте всю вещь BlockingQueue. Проверьте счетчик в конце, чтобы компилятор не оптимизировал операцию инкремента
Виктор Граци,
@grazi, вы можете сделать это в этом случае, но в большинстве реалистичных случаев вы этого не сделаете, так как ожидание на прилавке может быть неэффективным. Если бы вы сделали это, разница между примерами была бы еще больше.
Питер Лоури
31

Теоретически это зависит от JVM. На практике каждый поток имеет относительно большой объем стековой памяти (я думаю, 256 КБ по умолчанию). Кроме того, потоки реализованы как потоки ОС, поэтому для их создания требуется вызов ОС, то есть переключение контекста.

Поймите, что «дорогой» в вычислительной технике всегда очень относительный. Создание потока очень дорого по сравнению с созданием большинства объектов, но не очень дорого по сравнению со случайным поиском жесткого диска. Вам не нужно избегать создания потоков любой ценой, но создание сотен из них в секунду не является разумным шагом. В большинстве случаев, если ваш дизайн требует большого количества потоков, вы должны использовать пул потоков ограниченного размера.

Майкл Боргвардт
источник
9
Btw kb = килобит, kB = килобайт. Gb = гигабит, GB = гигабайт.
Питер Лоури
@PeterLawrey, пишем ли мы заглавные буквы «k» в «kb» и «kB», поэтому есть симметрия для «Gb» и «GB»? Эти вещи меня беспокоят.
Джек,
3
@ Джек Существует K= 1024 и k= 1000.;) en.wikipedia.org/wiki/Kibibyte
Питер Лори
9

Есть два вида потоков:

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

  2. «Зеленые» потоки : созданные и запланированные JVM, они дешевле, но никакого правильного паралеллизма не происходит. Они ведут себя как потоки, но выполняются в потоке JVM в ОС. Они не часто используются, насколько мне известно.

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

Помимо этого, создание потоков в основном зависит от ОС и даже от реализации виртуальной машины.

Теперь позвольте мне кое-что отметить: создание потоков стоит дорого, если вы планируете запускать 2000 потоков в секунду, каждую секунду вашего времени выполнения. JVM не предназначена для этого . Если у вас будет пара стабильных рабочих, которых не будут увольнять и убивать снова и снова, расслабьтесь.

slezica
источник
19
«... пару стабильных рабочих, которых не уволят и не убьют ...» Почему я начал думать об условиях труда? :-)
Стивен С
6

Создание Threadsтребует выделения достаточного количества памяти, так как он должен создать не один, а два новых стека (один для кода Java, один для собственного кода). Использование Executors / Thread Pools позволяет избежать накладных расходов, повторно используя потоки для нескольких задач для Executor .

Филипп JF
источник
@Raedwald, что такое jvm, который использует отдельные стеки?
bestsss
1
Филипп JP говорит 2 стека.
Raedwald
Насколько я знаю, все JVM выделяют два стека на поток. Для сборки мусора полезно обрабатывать Java-код (даже когда JITed) иначе, чем свободная приведение c.
Филипп JF
@Philip JF Можете ли вы уточнить? Что вы подразумеваете под 2 стеками, один для кода Java и один для нативного кода? Что оно делает?
Гуриндер
«Насколько я знаю, все JVM выделяют два стека на поток». - Я никогда не видел никаких доказательств, подтверждающих это. Возможно, вы неправильно понимаете истинную природу opstack в спецификации JVM. (Это способ моделирования поведения байт-кодов, а не то, что нужно использовать во время выполнения для их выполнения.)
Стивен С.
1

Очевидно, суть вопроса в том, что означает «дорогой».

Поток должен создать стек и инициализировать стек на основе метода run.

Для этого необходимо настроить управляющие структуры статуса, т. Е. В каком состоянии он находится в рабочем состоянии, ожидает и т. Д.

Вероятно, есть много синхронизации вокруг настройки этих вещей.

MeBigFatGuy
источник