Le thread virtuel Java est une nouvelle fonctionnalité disponible à partir de JDK 19. Il a le potentiel d’améliorer la disponibilité, le débit et la qualité du code de l’application en plus de réduire la consommation de mémoire.
Vidéo: Pour voir la présentation visuelle de cet article, cliquez ci-dessous :
Dans cet article, découvrons les pièges à éviter lors du passage des threads de la plate-forme Java aux threads virtuels :
1. Évitez les blocs/méthodes synchronisés
2. Évitez les pools de threads pour limiter l’accès aux ressources
3. Réduisez l’utilisation de ThreadLocal
Passons en revue ces pièges en détail.
1. Évitez les blocages/méthodes synchronisés
Lorsqu’une méthode est synchronisée en Java, un seul thread serait autorisé à entrer dans cette méthode à la fois. Considérons l’exemple ci-dessous :
1: public synchronized void getData() {
2:
3: makeDBCall();
4: }
Dans l’extrait de code ci-dessus, getData()
la méthode est synchronized
. Disons que thread-1 tente d’entrer dans le getData()
méthode d’abord. Pendant que ce thread-1 exécute le getData()
méthode, thread-2 tente d’exécuter cette méthode. Depuis thread-1
exécute actuellement le getData()
méthode, thread-2
ne sera pas autorisé à s’exécuter. Il sera mis dans l’état BLOQUÉ. Si vous utilisez un thread virtuel dans ce scénario, lorsque le thread passe à l’état BLOQUÉ, idéalement, il doit abandonner son contrôle du thread du système d’exploitation sous-jacent et revenir à la mémoire de tas. Cependant, en raison de la limitation de l’implémentation actuelle du thread virtuel, lorsqu’un thread virtuel est BLOQUÉ en raison d’une méthode synchronisée (ou d’un blocage), il n’abandonnera pas son contrôle sur le thread du système d’exploitation sous-jacent.
Dans ce cas, vous devriez envisager de remplacer les méthodes/blocs synchronisés par « ReentrantLock ». L’exemple de code ci-dessus synchronized getData()
la méthode peut être réécrite comme ceci en utilisant ReentrantLock
:
1: private ReentrantLock myLock = new ReentrantLock();
2:
3: public void getData() {
4:
5: myLock.lock(); // acquire lock
6: try {
7:
8: makeDBCall();
9: } finally {
10:
11: myLock.unlock(); // release lock
12: }
13: }
Lorsque vous remplacez la méthode synchronisée par ReentrantLock, le thread virtuel abandonne le contrôle du thread du système d’exploitation sous-jacent et vous pouvez profiter des avantages des threads virtuels.
Note: Le thread virtuel ne libérant pas le thread du système d’exploitation sous-jacent lorsque vous travaillez sur une méthode synchronisée est la limitation actuelle dans JDK 19. Il pourrait être résolu dans la future version de Java.
2. Évitez les pools de threads pour limiter l’accès aux ressources
Parfois, dans nos constructions de programmation, nous aurions pu utiliser un pool de threads pour limiter l’accès à certaines ressources. Supposons que nous voulions effectuer seulement 10 appels simultanés vers un système dorsal ; il a peut-être été programmé à l’aide d’un pool de threads, comme indiqué ci-dessous :
1: private ExecutorService BACKEND_THREAD_POOL = Executors.newFixedThreadPool(10);
2:
3: public <T> Future<T> queryBackend(Callable<T> query) {
4:
5: return BACKEND_THREAD_POOL.submit(query);
6: }
Dans la ligne #1, vous pouvez remarquer un BACKEND_THREAD_POOL
est créé avec 10 fils. Ce pool de threads est utilisé dans le queryBackend()
méthode pour effectuer des appels backend. Ce pool de threads garantira que pas plus de 10 appels simultanés seront effectués vers le système principal.
Au moment de la rédaction de cet article (janvier 2023), aucune API n’est disponible dans JDK pour créer un exécuteur (c’est-à-dire un pool de threads) avec un nombre fixe de threads virtuels. Voici la liste de toutes les API pour créer des threads virtuels. Lorsque vous utilisez Executor, vous ne pouvez créer que un nombre illimité de threads virtuels. Pour résoudre ce problème, vous pouvez envisager de remplacer Executor par Semaphore. Dans l’exemple ci-dessus, queryBackend()
méthode peut être réécrite en utilisant Semaphore
comme indiqué ci-dessous:
1: private static Semaphore BACKEND_SEMAPHORE = new Semaphore(10);
2:
3: public static <T> T queryBackend(Callable<T> query) throws Exception {
4:
5: BACKEND_SEMAPHORE.acquire(); // allow only 10 concurrent calls
6: try {
7:
8: return query.call();
9: } finally {
10:
11: BACKEND_SEMAPHORE.release();
12: }
13: }
Si vous remarquez à la ligne 1, nous créons un BACKEND_SEMAPHORE
avec 10 permis. Ce sémaphore n’autorisera que 10 appels simultanés au système principal. C’est une bonne alternative à l’exécuteur.
3. Réduire l’utilisation de ThreadLocal
Peu d’applications ont tendance à utiliser des variables ThreadLocal. En un mot, les variables Java ThreadLocal sont créées et stockées en tant que variables dans le cadre d’un seul thread particulier, et elles ne sont pas accessibles par d’autres threads. Si votre application crée des millions de threads virtuels et que chaque thread virtuel a sa propre variable ThreadLocal, elle peut rapidement consommer de l’espace mémoire de tas Java. Par conséquent, vous devez faire attention à la taille des données stockées en tant que variables ThreadLocal.
Vous vous demandez peut-être pourquoi les variables ThreadLocal ne sont pas problématiques dans les threads de plate-forme. La différence est que dans les threads de plate-forme, nous ne créons pas des millions de threads, alors que, dans les threads virtuels, nous le faisons. Des millions de threads, chacun avec sa propre copie de variables ThreadLocal, peuvent rapidement remplir la mémoire. On dit que de petites gouttes d’eau font un océan. C’est très vrai ici.
En général, les variables Java ThreadLocal sont difficiles à gérer et à maintenir. Cela peut également causer des problèmes de production désagréables. Ainsi, limiter la portée d’utilisation des variables ThreadLocal peut être bénéfique pour votre application, en particulier lors de l’utilisation de threads virtuels.