Questions d'entrevue multithread Java
Un processus est un environnement d'exécution autonome, qui peut être considéré comme un programme ou une application. Un thread est une tâche exécutée dans un processus. L'environnement d'exécution Java est un processus unique qui contient différentes classes et programmes. Les threads peuvent être appelés processus légers. Les threads nécessitent moins de ressources pour créer et résider dans un processus, et peuvent partager des ressources au sein du processus.
Dans un programme multithread, plusieurs threads sont exécutés simultanément pour améliorer l'efficacité du programme. Le processeur n'entrera pas dans un état inactif car un thread doit attendre des ressources. Plusieurs threads partagent la mémoire tas, il est donc préférable de créer plusieurs threads pour effectuer certaines tâches plutôt que de créer plusieurs processus. Par exemple, les servlets sont meilleurs que CGI car les servlets prennent en charge le multithreading, contrairement à CGI.
Lorsque nous créons un thread dans un programme Java, on l’appelle un thread utilisateur. Un thread démon est un thread qui s'exécute en arrière-plan et n'empêche pas la JVM de se terminer. Lorsqu'aucun thread utilisateur n'est en cours d'exécution, la JVM ferme le programme et se termine. Les threads enfants créés par un thread démon sont toujours des threads démon.
Il existe deux manières de créer un thread : l'une consiste à implémenter l'interface Runnable, puis à la transmettre au constructeur Thread pour créer un objet Thread ; l'autre consiste à hériter directement de la classe Thread ; Si vous souhaitez en savoir plus, vous pouvez lire cet article sur la façon de créer des threads en Java.
Lorsque nous créons un nouveau thread dans un programme Java, son statut est Nouveau. Lorsque nous appelons la méthode start() du thread, le statut passe à Runnable. Le planificateur de threads alloue du temps CPU aux threads du pool de threads exécutables et change leur statut en En cours d'exécution. Les autres états de thread incluent En attente, Bloqué et Mort. Lisez cet article pour en savoir plus sur le cycle de vie des threads.
Bien sûr, mais si nous appelons la méthode run() de Thread, elle se comportera comme une méthode normale. Afin d'exécuter notre code dans un nouveau thread, nous devons utiliser la méthode Thread.start().
Nous pouvons utiliser la méthode Sleep() de la classe Thread pour mettre le thread en pause pendant un certain temps. Il convient de noter que cela ne met pas fin au thread. Une fois que le thread est sorti du sommeil, le statut du thread sera changé en Runnable et il sera exécuté selon la planification du thread.
Chaque thread a une priorité. De manière générale, les threads de haute priorité auront la priorité lors de l'exécution, mais cela dépend de l'implémentation de la planification des threads, qui dépend du système d'exploitation. Nous pouvons définir la priorité des threads, mais cela ne garantit pas que les threads de haute priorité s'exécuteront avant les threads de faible priorité. La priorité du thread est une variable int (de 1 à 10), 1 représente la priorité la plus basse, 10 représente la priorité la plus élevée.
Le planificateur de threads est un service du système d'exploitation chargé d'allouer du temps CPU aux threads à l'état Exécutable. Une fois que nous avons créé un thread et l'avons démarré, son exécution dépend de l'implémentation du planificateur de threads. Le découpage temporel fait référence au processus d'allocation du temps CPU disponible aux threads exécutables disponibles. L'allocation du temps CPU peut être basée sur la priorité du thread ou sur le temps d'attente du thread. La planification des threads n'est pas contrôlée par la machine virtuelle Java, il est donc préférable que l'application la contrôle (c'est-à-dire ne faites pas dépendre votre programme de la priorité des threads).
La commutation de contexte est le processus de stockage et de restauration de l'état du processeur, qui permet à l'exécution du thread de reprendre l'exécution à partir du point d'interruption. Le changement de contexte est une fonctionnalité essentielle des systèmes d'exploitation multitâches et des environnements multithread.
Nous pouvons utiliser la méthode joint() de la classe Thread pour garantir que tous les threads créés par le programme se terminent avant la fin de la méthode main(). Voici un article sur la méthode joint() de la classe Thread.
Lorsque les ressources peuvent être partagées entre les threads, la communication entre les threads est un moyen important de les coordonner. Les méthodes wait()/notify()/notifyAll() de la classe Object peuvent être utilisées pour communiquer entre les threads sur l'état des verrous de ressources. Cliquez ici pour en savoir plus sur l'attente du fil de discussion, la notification et la notification à tous.
Chaque objet en Java possède un verrou (moniteur, qui peut également être un moniteur), et des méthodes telles que wait() et notify() sont utilisées pour attendre le verrou de l'objet ou avertir les autres threads que le moniteur de l'objet est disponible. Aucun verrou ou synchroniseur n'est disponible pour aucun objet dans les threads Java. C'est pourquoi ces méthodes font partie de la classe Object afin que chaque classe Java dispose de méthodes de base pour la communication entre les threads.
Lorsqu'un thread doit appeler la méthode wait() d'un objet, le thread doit posséder le verrou de l'objet. Ensuite, il libérera le verrou de l'objet et entrera dans l'état d'attente jusqu'à ce que d'autres threads appellent la méthode notify() sur l'objet. De même, lorsqu'un thread doit appeler la méthode notify() de l'objet, il libère le verrou de l'objet afin que les autres threads en attente puissent obtenir le verrou de l'objet. Étant donné que toutes ces méthodes nécessitent que le thread détienne le verrou de l'objet, ce qui ne peut être obtenu que par synchronisation, elles ne peuvent être appelées que dans des méthodes synchronisées ou des blocs synchronisés.
Les méthodes sleep() et rendement() de la classe Thread s'exécuteront sur le thread en cours d'exécution. Cela n’a donc aucun sens d’appeler ces méthodes sur d’autres threads en attente. C'est pourquoi ces méthodes sont statiques. Elles peuvent fonctionner dans le thread en cours d'exécution et empêcher les programmeurs de penser à tort que ces méthodes peuvent être appelées dans d'autres threads non en cours d'exécution.
Il existe de nombreuses façons de garantir la sécurité des threads en Java : synchronisation, utilisation de classes atomiques concurrentes, implémentation de verrous simultanés, utilisation du mot clé volatile, utilisation de classes immuables et de classes thread-safe. Vous pouvez en savoir plus dans le didacticiel sur la sécurité des threads.
Lorsque nous utilisons le mot-clé volatile pour modifier une variable, le thread lira directement la variable et ne la mettra pas en cache. Cela garantit que les variables lues par le thread sont cohérentes avec celles en mémoire.
Un bloc synchronisé est un meilleur choix car il ne verrouille pas l'intégralité de l'objet (bien sûr, vous pouvez également lui faire verrouiller l'intégralité de l'objet). Les méthodes synchronisées verrouillent l'intégralité de l'objet, même s'il existe plusieurs blocs synchronisés non liés dans la classe, ce qui entraîne généralement l'arrêt de leur exécution et la nécessité d'attendre pour obtenir le verrou sur l'objet.
Le thread peut être défini comme un thread démon en utilisant la méthode setDaemon(true) de la classe Thread. Il convient de noter que cette méthode doit être appelée avant d'appeler la méthode start(), sinon une IllegalThreadStateException sera levée.
ThreadLocal est utilisé pour créer des variables locales de thread. Nous savons que tous les threads d'un objet partageront ses variables globales, ces variables ne sont donc pas thread-safe. Mais lorsque nous ne voulons pas utiliser la synchronisation, nous pouvons choisir des variables ThreadLocal.
Chaque thread aura ses propres variables de thread et pourra utiliser les méthodes get()/set() pour obtenir leurs valeurs par défaut ou modifier leurs valeurs dans le thread. Les instances ThreadLocal souhaitent généralement que leur état de thread associé soit des propriétés statiques privées. Dans l'exemple d'article ThreadLocal, vous pouvez voir un petit programme sur ThreadLocal.
ThreadGroup est une classe dont le but est de fournir des informations sur les groupes de threads.
L'API ThreadGroup est relativement faible et ne fournit pas plus de fonctions que Thread. Il a deux fonctions principales : l'une consiste à obtenir la liste des threads actifs dans le groupe de threads ; l'autre est de définir le gestionnaire d'exceptions non capturées (gestionnaire d'exceptions naught) pour le thread. Cependant, dans Java 1.5, la classe Thread a également ajouté la méthode setUncaughtExceptionHandler(UncaughtExceptionHandler eh), donc ThreadGroup est obsolète et il n'est pas recommandé de continuer à l'utiliser.
t1.setUncaughtExceptionHandler(new UncaughtExceptionHandler(){ @Overridepublic void uncaughtException(Thread t, Throwable e) {System.out.println("exception s'est produite :"+e.getMessage());} });
Un thread dump est une liste de threads actifs JVM, ce qui est très utile pour analyser les goulots d'étranglement et les blocages du système. Il existe de nombreuses façons d'obtenir des thread dumps - en utilisant Profiler, la commande Kill -3, l'outil jstack, etc. Je préfère l'outil jstack car il est facile à utiliser et est livré avec JDK. Puisqu'il s'agit d'un outil basé sur un terminal, nous pouvons écrire des scripts pour générer périodiquement des thread dumps à des fins d'analyse. Lisez ce document pour en savoir plus sur la génération de thread dumps.
Le blocage fait référence à une situation dans laquelle plus de deux threads sont bloqués pour toujours. Cette situation nécessite au moins deux threads supplémentaires et plus de deux ressources.
Pour analyser le blocage, nous devons examiner le thread dump de l'application Java. Nous devons découvrir quels threads sont à l'état BLOQUÉ et quelles ressources ils attendent. Chaque ressource a un identifiant unique, en utilisant cet identifiant, nous pouvons découvrir quels threads possèdent déjà son verrou d'objet.
Éviter les verrous imbriqués, utiliser les verrous uniquement lorsque cela est nécessaire et éviter les attentes indéfinies sont des moyens courants d'éviter les blocages. Lisez cet article pour savoir comment analyser les blocages.
java.util.Timer est une classe d'outils qui peut être utilisée pour planifier l'exécution d'un thread à un moment précis dans le futur. La classe Timer peut être utilisée pour planifier des tâches ponctuelles ou des tâches périodiques.
java.util.TimerTask est une classe abstraite qui implémente l'interface Runnable. Nous devons hériter de cette classe pour créer nos propres tâches planifiées et utiliser Timer pour planifier son exécution.
Voici des exemples sur Java Timer.
Un pool de threads gère un groupe de threads de travail et comprend également une file d'attente pour placer les tâches en attente d'exécution.
java.util.concurrent.Executors fournit une implémentation de l'interface java.util.concurrent.Executor pour créer des pools de threads. L'exemple Thread Pool montre comment créer et utiliser un pool de threads, ou lisez l'exemple ScheduledThreadPoolExecutor pour savoir comment créer une tâche périodique.
Questions d'entretien sur la concurrence Java
Une opération atomique fait référence à une unité de tâche d’opération qui n’est pas affectée par d’autres opérations. Les opérations atomiques sont un moyen nécessaire pour éviter l'incohérence des données dans un environnement multithread.
int++ n'est pas une opération atomique, donc lorsqu'un thread lit sa valeur et ajoute 1, un autre thread peut lire la valeur précédente, ce qui provoquera une erreur.
Afin de résoudre ce problème, nous devons nous assurer que l'opération d'augmentation est atomique. Avant JDK1.5, nous pouvions utiliser la technologie de synchronisation pour ce faire. Depuis JDK 1.5, le package java.util.concurrent.atomic fournit des classes de chargement de type int et long qui garantissent automatiquement que leurs opérations sont atomiques et ne nécessitent pas l'utilisation de synchronisation. Vous pouvez lire cet article pour en savoir plus sur les classes atomiques de Java.
L'interface Lock fournit des opérations de verrouillage plus évolutives que les méthodes synchronisées et les blocs synchronisés. Ils permettent des structures plus flexibles qui peuvent avoir des propriétés complètement différentes et peuvent prendre en charge plusieurs classes associées d'objets conditionnels.
Ses avantages sont :
En savoir plus sur les exemples de verrouillage
Le framework Executor a été introduit dans Java 5 avec l'interface java.util.concurrent.Executor. Le framework Executor est un framework pour les tâches asynchrones qui sont appelées, planifiées, exécutées et contrôlées selon un ensemble de stratégies d'exécution.
La création illimitée de threads peut entraîner un débordement de la mémoire de l'application. Créer un pool de threads est donc une meilleure solution car le nombre de threads peut être limité et ces threads peuvent être recyclés et réutilisés. Il est très pratique de créer un pool de threads à l'aide du framework Executors. Lisez cet article pour savoir comment créer un pool de threads à l'aide du framework Executor.
Les caractéristiques de java.util.concurrent.BlockingQueue sont : lorsque la file d'attente est vide, l'opération d'obtention ou de suppression d'éléments de la file d'attente sera bloquée, ou lorsque la file d'attente est pleine, l'opération d'ajout d'éléments à la file d'attente sera bloquée. .
La file d'attente de blocage n'accepte pas les valeurs nulles. Lorsque vous essayez d'ajouter une valeur nulle à la file d'attente, elle lèvera une exception NullPointerException.
Les implémentations de files d'attente de blocage sont thread-safe et toutes les méthodes de requête sont atomiques et utilisent des verrous internes ou d'autres formes de contrôle de concurrence.
L'interface BlockingQueue fait partie du framework de collections Java et est principalement utilisée pour implémenter le problème producteur-consommateur.
Lisez cet article pour savoir comment implémenter le problème producteur-consommateur à l’aide de files d’attente bloquantes.
Java 5 a introduit l'interface java.util.concurrent.Callable dans le package de concurrence, qui est très similaire à l'interface Runnable, mais elle peut renvoyer un objet ou lever une exception.
L'interface Callable utilise des génériques pour définir son type de retour. La classe Executors fournit des méthodes utiles pour exécuter des tâches dans Callable dans le pool de threads. Puisque la tâche Callable est parallèle, nous devons attendre le résultat qu'elle renvoie. L'objet java.util.concurrent.Future résout ce problème pour nous. Une fois que le pool de threads a soumis la tâche Callable, un objet Future est renvoyé. En l'utilisant, nous pouvons connaître l'état de la tâche Callable et obtenir le résultat d'exécution renvoyé par le Callable. Future fournit la méthode get() afin que nous puissions attendre la fin du Callable et obtenir ses résultats d'exécution.
Lisez cet article pour en savoir plus sur Callable, Future.
FutureTask est une implémentation de base de Future, que nous pouvons utiliser avec Executors pour traiter des tâches asynchrones. Habituellement, nous n'avons pas besoin d'utiliser la classe FutureTask, mais cela devient très utile lorsque nous envisageons de remplacer certaines méthodes de l'interface Future et de conserver l'implémentation de base d'origine. Nous pouvons simplement en hériter et remplacer les méthodes dont nous avons besoin. Lisez l'exemple Java FutureTask pour savoir comment l'utiliser.
Les classes de collection Java sont rapides, ce qui signifie que lorsque la collection est modifiée et qu'un thread utilise un itérateur pour parcourir la collection, la méthode next() de l'itérateur lèvera une exception ConcurrentModificationException.
Les conteneurs simultanés prennent en charge les traversées simultanées et les mises à jour simultanées.
Les classes principales sont ConcurrentHashMap, CopyOnWriteArrayList et CopyOnWriteArraySet. Lisez cet article pour savoir comment éviter ConcurrentModificationException.
Les exécuteurs fournissent des méthodes utilitaires pour les classes Executor, ExecutorService, ScheduledExecutorService, ThreadFactory et Callable.
Les exécuteurs peuvent être utilisés pour créer facilement des pools de threads.
Texte original : journaldev.com Traduction : ifeve Traducteur : Zheng Xudong