Pourquoi créer un fil dit cher?

Les tutoriels Java indiquent que créer un thread est coûteux. Mais pourquoi exactement est-ce cher? Que se passe-t-il exactement lorsqu’un thread Java est créé, ce qui rend sa création coûteuse? Je prends la déclaration comme vraie, mais je suis juste intéressé par la mécanique de la création de threads dans JVM.

Frais généraux de cycle de vie des threads. La création et le déassembly des threads ne sont pas gratuits. La surcharge réelle varie selon les plates-formes, mais la création de threads prend du temps, ce qui introduit une latence dans le traitement des requêtes et nécessite une certaine activité de traitement de la part de la machine virtuelle Java et du système d’exploitation. Si les requêtes sont fréquentes et légères, comme dans la plupart des applications serveur, la création d’un nouveau thread pour chaque requête peut consumr des ressources informatiques importantes.

De la concurrence Java en pratique
Par Brian Goetz, Tim Peierls, Joshua Bloch, Joseph Bowbeer, David Holmes, Doug Lea
Imprimer ISBN-10: 0-321-34960-1

La création de threads Java est coûteuse car il y a pas mal de travail à faire:

  • Un grand bloc de mémoire doit être alloué et initialisé pour la stack de threads.
  • Des appels système doivent être effectués pour créer / enregistrer le thread natif avec le système d’exploitation hôte.
  • Les descripteurs doivent être créés, initialisés et ajoutés aux structures de données internes de la JVM.

Il est également coûteux en ce sens que le thread lie les ressources tant qu’il est vivant; Par exemple, la stack de threads, tous les objects accessibles depuis la stack, les descripteurs de threads JVM, les descripteurs de threads natifs du système d’exploitation.

Les coûts de toutes ces choses sont spécifiques à la plate-forme, mais ils ne sont pas bon marché sur les plates-formes Java que j’ai rencontrées.


Une recherche Google m’a permis de trouver un ancien benchmark indiquant un taux de création de threads de ~ 4000 par seconde sur un Sun Java 1.4.1 sur un Xeon 2002 à double processeur fonctionnant sous Linux vintage 2002. Une plate-forme plus moderne donnera de meilleurs chiffres… et je ne peux pas commenter la méthodologie… mais au moins cela donne une idée de la probabilité de création de threads.

Les parsings comparatives de Peter Lawrey indiquent que la création de threads est nettement plus rapide ces jours-ci en termes absolus, mais il est difficile de savoir dans quelle mesure cela est dû aux améliorations de Java et / ou du système d’exploitation … Mais ses chiffres indiquent toujours une amélioration de plus de 150 fois si vous utilisez un pool de threads plutôt que de créer / démarrer un nouveau thread à chaque fois. (Et il fait remarquer que c’est tout relatif …)


(Ce qui précède suppose des “threads natifs” plutôt que des “threads verts”, mais les machines virtuelles Java modernes utilisent toutes des threads natifs pour des raisons de performances. Les threads verts sont peut-être moins chers à créer,


J’ai un peu creusé pour voir comment la stack d’un thread Java est réellement allouée. Dans le cas d’OpenJDK 6 sous Linux, la stack de threads est allouée par l’appel à pthread_create qui crée le thread natif. (La machine virtuelle Java ne transmet pas pthread_create une stack préallouée.)

Ensuite, dans pthread_create la stack est allouée par un appel à mmap comme suit:

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

Selon man mmap , l’indicateur MAP_ANONYMOUS provoque l’initialisation de la mémoire à zéro.

Ainsi, même s’il n’est pas essentiel que les nouvelles stacks de threads Java soient mises à zéro (selon les spécifications JVM), en pratique (au moins avec OpenJDK 6 sous Linux), elles sont mises à zéro.

D’autres ont discuté de l’origine des coûts de filetage. Cette réponse explique pourquoi la création d’un thread n’est pas si coûteuse par rapport à de nombreuses opérations, mais relativement coûteuse par rapport aux alternatives d’exécution de tâches, qui sont relativement moins coûteuses.

L’alternative la plus évidente à l’exécution d’une tâche dans un autre thread consiste à exécuter la tâche dans le même thread. C’est difficile à comprendre pour ceux qui supposent que plus de threads sont toujours meilleurs. La logique est que si la surcharge liée à l’ajout de la tâche à un autre thread est supérieure au temps que vous enregistrez, l’exécution de la tâche dans le thread actuel peut être plus rapide.

Une autre solution consiste à utiliser un pool de threads. Un pool de threads peut être plus efficace pour deux raisons. 1) il réutilise les threads déjà créés. 2) vous pouvez régler / contrôler le nombre de threads pour vous assurer des performances optimales.

Le programme suivant imprime ….

 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 

Ceci est un test pour une tâche sortingviale qui expose la surcharge de chaque option de thread. (Cette tâche de test est le type de tâche le mieux exécutée dans le thread en cours.)

 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); } } } 

Comme vous pouvez le voir, la création d'un nouveau thread ne coûte que 70 µs environ. Cela pourrait être considéré comme sortingvial dans de nombreux cas d'utilisation, si ce n'est la plupart. Relativement parlant, il est plus coûteux que les alternatives et, pour certaines situations, un pool de threads ou une absence totale de threads est une meilleure solution.

En théorie, cela dépend de la JVM. En pratique, chaque thread a une quantité de mémoire de stack relativement importante (256 Ko par défaut, je pense). En outre, les threads sont implémentés en tant que threads de système d’exploitation, leur création implique donc un appel de système d’exploitation, c’est-à-dire un changement de contexte.

Se rendre compte que “cher” en informatique est toujours très relatif. La création de threads est très coûteuse par rapport à la création de la plupart des objects, mais pas très coûteuse par rapport à une recherche aléatoire sur disque dur. Vous n’avez pas à éviter de créer des threads à tout prix, mais en créer des centaines par seconde n’est pas une solution intelligente. Dans la plupart des cas, si votre conception nécessite beaucoup de threads, vous devez utiliser un pool de threads de taille limitée.

Il existe deux types de threads:

  1. Threads appropriés : ce sont des abstractions autour des fonctionnalités de threading du système d’exploitation sous-jacent. La création de fils est donc aussi coûteuse que celle du système – il y a toujours une surcharge.

  2. Les threads “verts” : créés et programmés par la JVM, ils sont moins chers, mais aucun paralellisme approprié ne se produit. Celles-ci se comportent comme des threads, mais sont exécutées dans le thread JVM dans le système d’exploitation. Ils ne sont pas souvent utilisés, à ma connaissance.

Le facteur le plus important auquel je peux penser dans la surcharge de création de threads est la taille de stack que vous avez définie pour vos threads. La taille de la stack de threads peut être transmise en tant que paramètre lors de l’exécution de la machine virtuelle.

En dehors de cela, la création de thread dépend principalement du système d’exploitation, et même de l’implémentation de VM.

Maintenant, permettez-moi de signaler quelque chose: créer des threads est coûteux si vous prévoyez de tirer 2000 threads par seconde, chaque seconde de votre runtime. La JVM n’est pas conçue pour gérer cela . Si vous avez un couple de travailleurs stables qui ne seront pas renvoyés et tués encore et encore, détendez-vous.

La création de Threads nécessite d’allouer une quantité importante de mémoire car elle doit créer non pas une, mais deux nouvelles stacks (une pour le code Java et une pour le code natif). L’utilisation d’ exécutants / de pools de threads permet d’éviter la surcharge en réutilisant les threads pour plusieurs tâches pour Executor .

De toute évidence, le nœud de la question est de savoir ce que signifie «cher».

Un thread doit créer une stack et initialiser la stack en fonction de la méthode d’exécution.

Il doit mettre en place des structures de statut de contrôle, c’est-à-dire dans quel état il est disponible, en attente, etc.

Il y a probablement beaucoup de synchronisation autour de la mise en place de ces choses.