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

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

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

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

Ответ 1

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

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

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

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


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

Бенчмаркинг Peter Lawrey указывает на то, что создание потоков в наши дни значительно ускоряется в абсолютном выражении, но неясно, насколько из-за этого улучшаются возможности Java и/или OS... или более высокие скорости процессора. Но его число по-прежнему указывает на улучшение в 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

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

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

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

Ответ 3

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

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

Ответ 4

Существует два вида потоков:

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

  • "Зеленые" потоки: созданы и запланированы JVM, они дешевле, но не возникает надлежащего паралеллизма. Они ведут себя как потоки, но выполняются в потоке JVM в ОС. Насколько мне известно, они часто не используются.

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

Кроме того, создание потоков в основном зависит от ОС и даже зависящих от VM.

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

Ответ 5

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

Ответ 6

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

В потоке необходимо создать стек и инициализировать стек на основе метода run.

Ему нужно настроить структуры состояния управления, то есть, какое состояние оно выполняется в ожидании и т.д.

Вероятно, существует большая синхронизация при настройке этих вещей.