Modèles de threads en Java

1. Introduction

Souvent, dans nos applications, nous devons être en mesure de faire plusieurs choses en même temps. Nous pouvons y parvenir de plusieurs manières, mais la clé d'entre elles est de mettre en œuvre le multitâche sous une forme ou une autre.

Le multitâche signifie exécuter plusieurs tâches en même temps , où chaque tâche effectue son travail. Ces tâches s'exécutent généralement toutes en même temps, lisant et écrivant la même mémoire et interagissant avec les mêmes ressources, mais faisant des choses différentes.

2. Threads natifs

La manière standard d'implémenter le multitâche en Java consiste à utiliser des threads . Le thread est généralement pris en charge jusqu'au système d'exploitation. Nous appelons les threads qui fonctionnent à ce niveau «threads natifs».

Le système d'exploitation a certaines capacités de threading qui ne sont souvent pas disponibles pour nos applications, simplement en raison de sa proximité avec le matériel sous-jacent. Cela signifie que l'exécution de threads natifs est généralement plus efficace. Ces threads mappent directement sur les threads d'exécution sur le processeur de l'ordinateur - et le système d'exploitation gère le mappage des threads sur les cœurs du processeur.

Le modèle de threading standard en Java, couvrant tous les langages JVM, utilise des threads natifs . C'est le cas depuis Java 1.2 et c'est le cas quel que soit le système sous-jacent sur lequel la JVM s'exécute.

Cela signifie que chaque fois que nous utilisons l'un des mécanismes de threading standard de Java, nous utilisons des threads natifs. Cela inclut java.lang.Thread , java.util.concurrent.Executor , java.util.concurrent.ExecutorService , etc.

3. Fils verts

En génie logiciel, une alternative aux threads natifs est les threads verts . C'est là que nous utilisons les threads, mais ils ne correspondent pas directement aux threads du système d'exploitation. Au lieu de cela, l'architecture sous-jacente gère les threads elle-même et gère la manière dont ils sont mappés aux threads du système d'exploitation.

En règle générale, cela fonctionne en exécutant plusieurs threads natifs, puis en allouant les threads verts sur ces threads natifs pour exécution . Le système peut alors choisir les threads verts actifs à un moment donné et les threads natifs sur lesquels ils sont actifs.

Cela semble très compliqué, et ça l'est. Mais c'est une complication dont nous n'avons généralement pas besoin de nous soucier. L'architecture sous-jacente s'occupe de tout cela et nous pouvons l'utiliser comme s'il s'agissait d'un modèle de thread natif.

Alors pourquoi ferions-nous cela? Les threads natifs sont très efficaces à exécuter, mais leur démarrage et leur arrêt coûtent très cher. Les threads verts permettent d'éviter ce coût et donnent à l'architecture beaucoup plus de flexibilité. Si nous utilisons des threads relativement longs, alors les threads natifs sont très efficaces. Pour les emplois de très courte durée, le coût de leur démarrage peut l'emporter sur les avantages de leur utilisation . Dans ces cas, les fils verts peuvent devenir plus efficaces.

Malheureusement, Java n'a pas de support intégré pour les threads verts.

Les toutes premières versions utilisaient des threads verts au lieu de threads natifs comme modèle de threading standard. Cela a changé dans Java 1.2, et il n'y a plus eu de support pour cela au niveau JVM depuis.

Il est également difficile d'implémenter des threads verts dans les bibliothèques car ils auraient besoin d'un support de très bas niveau pour fonctionner correctement. En tant que tel, une alternative couramment utilisée est les fibres.

4. Fibres

Les fibres sont une forme alternative de multi-threading et sont similaires aux fils verts . Dans les deux cas, nous n'utilisons pas de threads natifs et utilisons à la place les contrôles système sous-jacents qui s'exécutent à tout moment. La grande différence entre les fils verts et les fibres réside dans le niveau de contrôle, et plus précisément qui contrôle.

Les fils verts sont une forme de multitâche préventif. Cela signifie que l'architecture sous-jacente est entièrement responsable du choix des threads en cours d'exécution à un moment donné.

Cela signifie que tous les problèmes habituels de threading s'appliquent, où nous ne savons rien de l'ordre d'exécution de nos threads, ou lesquels seront exécutés en même temps. Cela signifie également que le système sous-jacent doit pouvoir mettre en pause et redémarrer notre code à tout moment, potentiellement au milieu d'une méthode ou même d'une instruction.

Les fibres sont plutôt une forme de multitâche coopérative, ce qui signifie qu'un thread en cours continuera à fonctionner jusqu'à ce qu'il signale qu'il peut céder à un autre . Cela signifie qu'il est de notre responsabilité que les fibres coopèrent les unes avec les autres. Cela nous donne un contrôle direct sur le moment où les fibres peuvent interrompre l'exécution, au lieu que le système décide cela pour nous.

Cela signifie également que nous devons écrire notre code d'une manière qui le permet. Sinon, cela ne fonctionnera pas. Si notre code n'a aucun point d'interruption, alors nous pourrions aussi bien ne pas utiliser du tout de fibres.

Java n'a actuellement pas de support intégré pour les fibres. Certaines bibliothèques existent qui peuvent introduire cela dans nos applications, y compris mais sans s'y limiter:

4.1. Quasar

Quasar est une bibliothèque Java qui fonctionne bien avec Java pur et Kotlin et a une version alternative qui fonctionne avec Clojure.

Cela fonctionne en ayant un agent Java qui doit fonctionner avec l'application, et cet agent est chargé de gérer les fibres et de s'assurer qu'elles fonctionnent correctement ensemble. L'utilisation d'un agent Java signifie qu'aucune étape de construction spéciale n'est nécessaire.

Quasar nécessite également que Java 11 fonctionne correctement, ce qui peut limiter les applications qui peuvent l'utiliser. Les versions plus anciennes peuvent être utilisées sur Java 8, mais elles ne sont pas activement prises en charge.

4.2. Kilim

Kilim est une bibliothèque Java qui offre des fonctionnalités très similaires à Quasar mais le fait en utilisant le tissage de bytecode au lieu d'un agent Java . Cela signifie qu'il peut fonctionner dans plus d'endroits, mais cela rend le processus de construction plus compliqué.

Kilim fonctionne avec Java 7 et plus récent et fonctionnera correctement même dans les scénarios où un agent Java n'est pas une option. Par exemple, si un autre est déjà utilisé pour l'instrumentation ou la surveillance.

4.3. Projet Loom

Project Loom est une expérience du projet OpenJDK pour ajouter des fibres à la JVM elle-même, plutôt qu'en tant que bibliothèque complémentaire . Cela nous donnera les avantages des fibres par rapport aux fils. En l'implémentant directement sur la JVM, cela peut aider à éviter les complications que les agents Java et le tissage de bytecode introduisent.

Il n'y a pas de calendrier de publication actuel pour Project Loom, mais nous pouvons télécharger les binaires d'accès anticipé dès maintenant pour voir comment les choses se passent. Cependant, comme il est encore très tôt, nous devons être prudents en nous appuyant sur cela pour tout code de production.

5. Co-routines

Les co-routines sont une alternative au filetage et aux fibres. Nous pouvons considérer les co-routines comme des fibres sans aucune forme d'ordonnancement . Au lieu que le système sous-jacent décide des tâches à effectuer à tout moment, notre code le fait directement.

Généralement, nous écrivons des co-routines pour qu'elles cèdent à des points spécifiques de leur flux. Ceux-ci peuvent être considérés comme des points de pause dans notre fonction, où il cessera de fonctionner et produira potentiellement un résultat intermédiaire. Lorsque nous cédons, nous sommes alors arrêtés jusqu'à ce que le code appelant décide de nous redémarrer pour une raison quelconque. Cela signifie que notre code d'appel contrôle la planification du moment où cela s'exécutera.

Kotlin a un support natif pour les co-routines intégrées dans sa bibliothèque standard. Il existe plusieurs autres bibliothèques Java que nous pouvons également utiliser pour les implémenter si vous le souhaitez.

6. Conclusion

Nous avons vu plusieurs alternatives différentes pour le multitâche dans notre code, allant des threads natifs traditionnels à des alternatives très légères. Pourquoi ne pas les essayer la prochaine fois qu'une application aura besoin de simultanéité?