Perché la creazione di una discussione è costosa?

Le esercitazioni Java dicono che la creazione di una discussione è costosa. Ma perché è esattamente costoso? Che cosa sta succedendo esattamente quando viene creato un thread Java che rende la sua creazione costosa? Sto prendendo la dichiarazione come vera, ma sono solo interessato alla meccanica della creazione di Thread in JVM.

Termine del ciclo di vita del thread. La creazione di thread e il teardown non sono gratuiti. L’overhead effettivo varia su tutte le piattaforms, ma la creazione dei thread richiede tempo, introduce la latenza nell’elaborazione delle richieste e richiede alcune attività di elaborazione da parte della JVM e del sistema operativo. Se le richieste sono frequenti e leggere, come nella maggior parte delle applicazioni server, la creazione di un nuovo thread per ciascuna richiesta può consumare notevoli risorse di elaborazione.

Dalla concorrenza di Java in pratica
Di Brian Goetz, Tim Peierls, Joshua Bloch, Joseph Bowbeer, David Holmes, Doug Lea
Stampa ISBN-10: 0-321-34960-1

La creazione di thread Java è costosa perché c’è un bel po ‘di lavoro coinvolto:

  • Un grande blocco di memoria deve essere allocato e inizializzato per lo stack di thread.
  • È necessario effettuare chiamate di sistema per creare / registrare il thread nativo con il sistema operativo host.
  • I descrittori devono essere creati, inizializzati e aggiunti alle strutture di dati interne JVM.

È anche costoso, nel senso che il filo lega le risorse finché è vivo; ad es. lo stack di thread, qualsiasi object raggiungibile dallo stack, i descrittori di thread JVM, i descrittori di thread nativi del sistema operativo.

I costi di tutte queste cose sono specifici della piattaforma, ma non sono economici su nessuna piattaforma Java che abbia mai visto.


Una ricerca su Google mi ha trovato un vecchio benchmark che riporta un tasso di creazione di thread di ~ 4000 al secondo su Sun Java 1.4.1 su un Xeon vintage 2002 con processore 2002 con Linux vintage. Una piattaforma più moderna darà numeri migliori … e non posso commentare la metodologia … ma almeno dà un campo da baseball per quanto è probabile che la creazione di thread sia costosa .

Il benchmarking di Peter Lawrey indica che la creazione di thread è significativamente più veloce al giorno d’oggi in termini assoluti, ma non è chiaro quanto di questi miglioramenti siano dovuti a Java e / o al sistema operativo … oa velocità del processore più elevate. Ma i suoi numeri indicano ancora un miglioramento di 150+ volte se si utilizza un pool di thread rispetto alla creazione / avvio di un nuovo thread ogni volta. (E afferma che questo è tutto relativo …)


(Quanto sopra presuppone “thread nativi” anziché “fili verdi”, ma i JVM moderni utilizzano tutti i thread nativi per motivi di prestazioni. I thread verdi sono probabilmente più economici da creare, ma si paga in altre aree.)


Ho fatto un po ‘di scavo per vedere come viene realmente allocato uno stack di thread Java. Nel caso di OpenJDK 6 su Linux, lo stack thread è allocato dalla chiamata a pthread_create che crea il thread nativo. (La JVM non supera pthread_create uno stack preallocato).

Quindi, all’interno di pthread_create lo stack viene allocato da una chiamata a mmap come segue:

 mmap(0, attr.__stacksize, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) 

Secondo man mmap , il flag MAP_ANONYMOUS fa sì che la memoria venga inizializzata a zero.

Quindi, anche se potrebbe non essere essenziale che i nuovi stack di thread Java siano azzerati (secondo le specifiche JVM), in pratica (almeno con OpenJDK 6 su Linux) vengono azzerati.

Altri hanno discusso di dove provengono i costi del threading. Questa risposta spiega perché la creazione di una discussione non è costosa rispetto a molte operazioni, ma relativamente costosa rispetto alle alternative di esecuzione delle attività, che sono relativamente meno costose.

L’alternativa più ovvia all’esecuzione di un’attività in un altro thread è eseguire l’attività nello stesso thread. Questo è difficile da capire per quelli che presuppongono che più thread siano sempre migliori. La logica è che se l’overhead di aggiungere l’attività a un altro thread è maggiore del tempo di salvataggio, può essere più veloce eseguire l’attività nel thread corrente.

Un’altra alternativa è usare un pool di thread. Un pool di thread può essere più efficiente per due motivi. 1) riutilizza i thread già creati. 2) è ansible sintonizzare / controllare il numero di thread per garantire prestazioni ottimali.

Il seguente programma stampa ….

 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 

Questo è un test per un compito banale che espone il sovraccarico di ogni opzione di threading. (Questa attività di test è il tipo di attività che viene effettivamente eseguita nel thread corrente.)

 final BlockingQueue queue = new LinkedBlockingQueue(); 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); } } } 

Come puoi vedere, la creazione di un nuovo thread costa solo ~ 70 μs. Questo potrebbe essere considerato banale in molti, se non nella maggior parte, casi d'uso. Relativamente parlando è più costoso delle alternative e per alcune situazioni un pool di thread o non utilizzare thread è una soluzione migliore.

In teoria, ciò dipende dalla JVM. In pratica, ogni thread ha una quantità relativamente grande di memoria di stack (credo che siano 256 KB per impostazione predefinita). Inoltre, i thread sono implementati come thread del sistema operativo, quindi la loro creazione implica una chiamata del sistema operativo, ovvero un cambio di contesto.

Capire che “costoso” nel calcolo è sempre molto relativo. La creazione di thread è molto costosa rispetto alla creazione della maggior parte degli oggetti, ma non molto costosa rispetto a una ricerca casuale di dischi rigidi. Non devi evitare di creare discussioni a tutti i costi, ma la creazione di centinaia di essi al secondo non è una mossa intelligente. Nella maggior parte dei casi, se il tuo progetto richiede un sacco di thread, dovresti utilizzare un pool di thread di dimensioni limitate.

Esistono due tipi di thread:

  1. Thread appropriati : si tratta di astrazioni attorno alle strutture di threading del sistema operativo sottostante. La creazione di thread è, quindi, costosa come quella del sistema – c’è sempre un sovraccarico.

  2. Fili “verdi” : creati e programmati dalla JVM, sono più economici, ma non si verifica un vero e proprio paralellismo. Questi si comportano come i thread, ma vengono eseguiti all’interno del thread JVM nel sistema operativo. Non sono spesso usati, per quanto ne so.

Il più grande fattore che posso pensare nel sovraccarico di creazione dei thread, è la dimensione dello stack che hai definito per i tuoi thread. La dimensione dello stack del thread può essere passata come parametro durante l’esecuzione della VM.

Oltre a ciò, la creazione del thread dipende principalmente dal SO e anche dall’implementazione della VM.

Ora, permettetemi di indicare qualcosa: la creazione di thread è costosa se state pianificando l’triggerszione di 2000 thread al secondo, ogni secondo del vostro runtime. La JVM non è progettata per gestirli . Se avrai un paio di lavoratori stabili che non saranno licenziati e uccisi più e più volte, rilassati.

La creazione di Threads richiede l’allocazione di una buona quantità di memoria poiché non deve fare uno, ma due nuovi stack (uno per il codice java, uno per il codice nativo). L’utilizzo di Executor / Thread Pools può evitare l’overhead, riutilizzando i thread per più attività per Executor .

Ovviamente il punto cruciale della domanda è cosa significa “costoso”.

Un thread deve creare uno stack e inizializzare lo stack in base al metodo run.

Ha bisogno di impostare le strutture dello stato di controllo, cioè quale stato è eseguibile, in attesa, ecc.

Probabilmente c’è una buona dose di sincronizzazione attorno all’impostazione di queste cose.