Questions d’entretien d’embauche Java (réponses)

Questions d'entretiens chez Java Concurrency (+ Réponses)

1. introduction

La simultanéité en Java est l’un des sujets les plus complexes et les plus avancés abordés lors des entretiens techniques. Cet article fournit des réponses à certaines des questions de l'entrevue sur le sujet que vous pouvez rencontrer.

Q1. Quelle est la différence entre un processus et un thread?

Les processus et les threads sont des unités de simultanéité, mais ils ont une différence fondamentale: les processus ne partagent pas de mémoire commune, contrairement aux threads.

Du point de vue du système d’exploitation, un processus est un logiciel indépendant qui s’exécute dans son propre espace de mémoire virtuelle. Tout système d’exploitation multitâche (ce qui signifie presque tous les systèmes d’exploitation modernes) doit séparer les processus en mémoire afin qu’un processus défaillant ne ralentisse pas tous les autres processus en brouillant la mémoire commune.

Les processus sont donc généralement isolés et coopèrent par le biais d'une communication inter-processus définie par le système d'exploitation comme une sorte d'API intermédiaire.

Au contraire, un thread est une partie d'une application qui partage une mémoire commune avec d'autres threads de la même application. L'utilisation de la mémoire commune permet de réduire considérablement les frais généraux, de concevoir les threads de manière à coopérer et à échanger des données entre eux beaucoup plus rapidement.

Q2. Comment créer une instance de thread et l'exécuter?

Pour créer une instance d'un fil, vous avez deux options. Commencez par transmettre une instance deRunnable à son constructeur et appelezstart(). Runnable est une interface fonctionnelle, elle peut donc être passée en tant qu'expression lambda:

Thread thread1 = new Thread(() ->
  System.out.println("Hello World from Runnable!"));
thread1.start();

Thread implémente égalementRunnable, donc une autre façon de démarrer un thread est de créer une sous-classe anonyme, de remplacer sa méthoderun(), puis d'appelerstart():

Thread thread2 = new Thread() {
    @Override
    public void run() {
        System.out.println("Hello World from subclass!");
    }
};
thread2.start();

Q3. Décrivez les différents états d'un thread et quand se produisent les transitions d'état.

L'état d'unThread peut être vérifié en utilisant la méthodeThread.getState(). Différents états d'unThread sont décrits dans l'énumérationThread.State. Elles sont:

  • NEW - une nouvelle instance deThread qui n'a pas encore été démarrée viaThread.start()

  • RUNNABLE - un thread en cours d'exécution. Il est appelé exécutable car à tout moment, il peut être en cours d'exécution ou attendre le prochain quantum de temps du planificateur de threads. Un threadNEW entre dans l'étatRUNNABLE lorsque vous appelezThread.start() dessus

  • BLOCKED - un thread en cours d'exécution est bloqué s'il doit entrer dans une section synchronisée mais ne peut pas le faire en raison d'un autre thread tenant le moniteur de cette section

  • WAITING - un thread entre dans cet état s'il attend qu'un autre thread exécute une action particulière. Par exemple, un thread entre dans cet état en appelant la méthodeObject.wait() sur un moniteur qu'il contient, ou la méthodeThread.join() sur un autre thread

  • TIMED_WAITING - identique à ci-dessus, mais un thread entre dans cet état après avoir appelé des versions chronométrées deThread.sleep(),Object.wait(),Thread.join() et quelques autres méthodes

  • TERMINATED - un thread a terminé l'exécution de sa méthodeRunnable.run() et s'est terminé

Q4. Quelle est la différence entre les interfaces exécutables et appelables? Comment sont-ils utilisés?

L'interfaceRunnable a une seule méthoderun. Cela représente une unité de calcul qui doit être exécutée dans un thread séparé. L'interfaceRunnable ne permet pas à cette méthode de renvoyer une valeur ou de lever des exceptions non vérifiées.

L'interfaceCallable a une seule méthodecall et représente une tâche qui a une valeur. C’est pourquoi la méthodecall renvoie une valeur. Il peut aussi jeter des exceptions. Callable est généralement utilisé dans les instancesExecutorService pour démarrer une tâche asynchrone, puis appeler l'instanceFuture retournée pour obtenir sa valeur.

Q5. Qu'est-ce qu'un thread Daemon, quels sont ses cas d'utilisation? Comment créer un fil de discussion démon?

Un thread démon est un thread qui n'empêche pas la machine virtuelle Java de quitter. Lorsque tous les threads non démon sont terminés, la machine virtuelle Java abandonne simplement tous les threads restants du démon. Les threads de démon sont généralement utilisés pour effectuer des tâches de support ou de service pour d'autres threads, mais vous devez savoir qu'ils peuvent être abandonnés à tout moment.

Pour démarrer un thread en tant que démon, vous devez utiliser la méthodesetDaemon() avant d'appelerstart():

Thread daemon = new Thread(()
  -> System.out.println("Hello from daemon!"));
daemon.setDaemon(true);
daemon.start();

Curieusement, si vous exécutez ceci dans le cadre de la méthodemain(), le message risque de ne pas être imprimé. Cela pourrait arriver si le thread demain() se terminait avant que le démon n'atteigne le point d'imprimer le message. Vous ne devriez généralement pas effectuer d’E / S dans les threads démons, car ils ne pourront même pas exécuter leurs blocsfinally et fermer les ressources s’ils sont abandonnés.

Q6. Qu'est-ce que le drapeau d'interruption du thread? Comment pouvez-vous régler et vérifier? Quel est le lien avec l'exception interrompue?

L'indicateur d'interruption, ou état d'interruption, est un indicateur interneThread qui est défini lorsque le thread est interrompu. Pour le définir, appelez simplementthread.interrupt() sur l'objet thread.

Si un thread est actuellement dans l'une des méthodes qui lancentInterruptedException (wait,join,sleep etc.), alors cette méthode lève immédiatement InterruptedException. Le thread est libre de traiter cette exception selon sa propre logique.

Si un thread n'est pas dans une telle méthode et quethread.interrupt() est appelé, rien de spécial ne se produit. Il est de la responsabilité du thread de vérifier périodiquement l'état de l'interruption en utilisant la méthodestatic Thread.interrupted() ou instanceisInterrupted(). La différence entre ces méthodes est questatic Thread.interrupt() efface l'indicateur d'interruption, alors queisInterrupted() ne le fait pas.

Q7. Que sont l'exécuteur et le service des exécuteurs? Quelles sont les différences entre ces interfaces?

Executor etExecutorService sont deux interfaces liées du frameworkjava.util.concurrent. Executor est une interface très simple avec une seule méthodeexecute acceptant les instancesRunnable pour l'exécution. Dans la plupart des cas, il s'agit de l'interface sur laquelle doit dépendre votre code d'exécution de tâches.

ExecutorService étend l'interfaceExecutor avec plusieurs méthodes pour gérer et vérifier le cycle de vie d'un service d'exécution de tâches simultanées (arrêt des tâches en cas d'arrêt) et des méthodes pour la gestion des tâches asynchrones plus complexes, y comprisFutures.

Pour plus d'informations sur l'utilisation deExecutor etExecutorService, consultez l'articleA Guide to Java ExecutorService.

Q8. Quelles sont les implémentations disponibles de Executorservice dans la bibliothèque standard?

L'interfaceExecutorService a trois implémentations standard:

  • ThreadPoolExecutor - pour exécuter des tâches à l'aide d'un pool de threads. Une fois qu'un thread a fini d'exécuter la tâche, il retourne dans le pool. Si tous les threads du pool sont occupés, la tâche doit attendre son tour.

  • ScheduledThreadPoolExecutor permet de planifier l'exécution de la tâche au lieu de l'exécuter immédiatement lorsqu'un thread est disponible. Il peut également planifier des tâches avec un taux fixe ou un délai fixe.

  • ForkJoinPool est unExecutorService spécial pour traiter les tâches d'algorithmes récursifs. Si vous utilisez unThreadPoolExecutor normal pour un algorithme récursif, vous trouverez rapidement que tous vos threads sont occupés à attendre la fin des niveaux inférieurs de récursivité. LeForkJoinPool implémente le soi-disant algorithme de vol de travail qui lui permet d'utiliser plus efficacement les threads disponibles.

Q9. Qu'est-ce que le modèle de mémoire Java (Jmm)? Décrivez son objectif et ses idées de base.

Le modèle de mémoire Java fait partie de la spécification du langage Java décrite dansChapter 17.4. Il spécifie comment plusieurs threads accèdent à la mémoire commune dans une application Java simultanée et comment les modifications de données effectuées par un thread sont rendues visibles par d'autres threads. Tout en étant assez court et concis, JMM peut être difficile à comprendre sans une base mathématique solide.

Le besoin de modèle de mémoire découle du fait que la manière dont votre code Java accède aux données n'est pas comme cela se passe réellement aux niveaux inférieurs. Les écritures et les lectures en mémoire peuvent être réorganisées ou optimisées par le compilateur Java, le compilateur JIT et même le processeur, à condition que le résultat observable de ces lectures et écritures soit identique.

Cela peut conduire à des résultats contre-intuitifs lorsque votre application est dimensionnée à plusieurs threads, car la plupart de ces optimisations prennent en compte un seul thread d'exécution (les optimiseurs cross-thread sont encore extrêmement difficiles à implémenter). Un autre problème important est que la mémoire dans les systèmes modernes est multicouche: plusieurs cœurs d’un processeur peuvent conserver des données non vidées dans leurs caches ou tampons de lecture / écriture, ce qui affecte également l’état de la mémoire observée à partir d’autres cœurs.

Pour aggraver les choses, l’existence de différentes architectures d’accès à la mémoire briserait la promesse de Java «d’écrire une fois, de courir partout». Heureusement pour les programmeurs, JMM spécifie certaines garanties sur lesquelles vous pouvez compter lors de la conception d'applications multithread. Le respect de ces garanties permet au programmeur d’écrire un code multithread stable et portable entre différentes architectures.

Les principales notions de JMM sont:

  • Actions, ce sont des actions inter-threads qui peuvent être exécutées par un thread et détectées par un autre thread, comme la lecture ou l'écriture de variables, le verrouillage / déverrouillage des moniteurs, etc.

  • Synchronization actions, un certain sous-ensemble d'actions, comme lire / écrire une variablevolatile ou verrouiller / déverrouiller un moniteur

  • Program Order (PO), l'ordre total observable des actions dans un seul thread

  • Synchronization Order (SO), l'ordre total entre toutes les actions de synchronisation - il doit être cohérent avec l'ordre du programme, c'est-à-dire que si deux actions de synchronisation se succèdent dans PO, elles se produisent dans le même ordre dans SO

  • Relationsynchronizes-with (SW) entre certaines actions de synchronisation, comme le déverrouillage du moniteur et le verrouillage du même moniteur (dans un autre ou le même thread)

  • Happens-before Order - combine PO avec SW (cela s'appelletransitive closure en théorie des ensembles) pour créer un ordre partiel de toutes les actions entre les threads. Si une actionhappens-beforeest une autre, alors les résultats de la première action sont observables par la seconde action (par exemple, écrire une variable dans un thread et lire dans un autre)

  • Happens-before consistency - un ensemble d'actions est cohérent avec HB si chaque lecture observe soit la dernière écriture à cet emplacement dans l'ordre qui se produit avant, soit une autre écriture via la course aux données

  • Execution - un certain ensemble d'actions ordonnées et de règles de cohérence entre elles

Pour un programme donné, nous pouvons observer plusieurs exécutions différentes avec différents résultats. Mais si un programme estcorrectly synchronized, alors toutes ses exécutions semblent êtresequentially consistent, ce qui signifie que vous pouvez raisonner sur le programme multithread comme un ensemble d'actions se produisant dans un ordre séquentiel. Cela vous évite d'avoir à vous soucier des réorganisations, des optimisations ou de la mise en cache des données.

Q10. Qu'est-ce qu'un champ volatil et quelles garanties le Jmm détient-il pour un tel champ?

Un champvolatile a des propriétés spéciales selon le modèle de mémoire Java (voir Q9). Les lectures et écritures d'une variablevolatile sont des actions de synchronisation, ce qui signifie qu'elles ont un ordre total (tous les threads observeront un ordre cohérent de ces actions). Une lecture d'une variable volatile est garantie pour observer la dernière écriture dans cette variable, selon cet ordre.

Si vous avez un champ accessible à partir de plusieurs threads, avec au moins un thread qui y écrit, alors vous devriez envisager de le rendrevolatile, sinon il y a une petite garantie sur ce qu'un certain thread lirait à partir de ce champ .

Une autre garantie pourvolatile est l'atomicité de l'écriture et de la lecture des valeurs 64 bits (long etdouble). Sans modificateur volatile, une lecture de ce champ pourrait observer une valeur partiellement écrite par un autre thread.

Q11. Lesquelles des opérations suivantes sont atomiques?

  • écriture dans un non -volatileint;

  • écrire dans unvolatile int;

  • écriture dans un non -volatile long;

  • écrire dans unvolatile long;

  • incrémenter unvolatile long?

Une écriture dans une variableint (32 bits) est garantie d'être atomique, qu'elle soitvolatile ou non. Une variablelong (64 bits) peut être écrite en deux étapes distinctes, par exemple sur des architectures 32 bits, donc par défaut, il n'y a pas de garantie d'atomicité. Cependant, si vous spécifiez le modificateurvolatile, une variablelong est garantie d'être accessible de manière atomique.

L'opération d'incrémentation est généralement effectuée en plusieurs étapes (récupérer une valeur, la modifier et la réécrire), de sorte qu'elle n'est jamais garantie d'être atomique, que la variable soitvolatile ou non. Si vous devez implémenter l'incrémentation atomique d'une valeur, vous devez utiliser les classesAtomicInteger,AtomicLong etc.

Q12. Quelles garanties spéciales le Jmm détient-il pour les champs finaux d'une classe?

La JVM garantit fondamentalement que les champsfinal d'une classe seront initialisés avant qu'un thread ne prenne possession de l'objet. Sans cette garantie, une référence à un objet peut être publiée, c'est-à-dire deviennent visibles, à un autre thread avant que tous les champs de cet objet ne soient initialisés, en raison de réorganisations ou d'autres optimisations. Cela pourrait entraîner un accès rapide à ces champs.

C'est pourquoi, lors de la création d'un objet immuable, vous devez toujours rendre tous ses champsfinal, même s'ils ne sont pas accessibles via les méthodes getter.

Q13. Quelle est la signification d'un mot clé synchronisé dans la définition d'une méthode? d'une méthode statique? Avant un blocage?

Le mot clésynchronized avant un bloc signifie que tout thread entrant dans ce bloc doit acquérir le moniteur (l'objet entre parenthèses). Si le moniteur est déjà acquis par un autre thread, l'ancien thread entrera dans l'étatBLOCKED et attendra que le moniteur soit libéré.

synchronized(object) {
    // ...
}

Une méthode d'instancesynchronized a la même sémantique, mais l'instance elle-même agit comme un moniteur.

synchronized void instanceMethod() {
    // ...
}

Pour une méthodestatic synchronized, le moniteur est l'objetClass représentant la classe déclarante.

static synchronized void staticMethod() {
    // ...
}

Q14. Si deux threads appellent simultanément une méthode synchronisée sur différentes instances d'objet, l'un de ces threads peut-il se bloquer? Et si la méthode est statique?

Si la méthode est une méthode d'instance, l'instance fait alors office de moniteur pour la méthode. Deux threads appelant la méthode sur différentes instances acquièrent des moniteurs différents, donc aucun d'entre eux n'est bloqué.

Si la méthode eststatic, alors le moniteur est l'objetClass. Pour les deux threads, le moniteur est le même, donc l'un d'eux bloquera probablement et attendra un autre pour quitter la méthodesynchronized.

Q15. À quoi servent les méthodes Wait, Notify et Notifyall de la classe d'objets?

Un thread qui possède le moniteur de l'objet (par exemple, un thread qui est entré dans une sectionsynchronized gardée par l'objet) peut appelerobject.wait() pour libérer temporairement le moniteur et donner à d'autres threads une chance d'acquérir le moniteur . Cela peut être fait, par exemple, pour attendre une certaine condition.

Lorsqu'un autre thread qui a acquis le moniteur remplit la condition, il peut appelerobject.notify() ouobject.notifyAll() et libérer le moniteur. La méthodenotify réveille un seul thread dans l'état d'attente, et la méthodenotifyAll réveille tous les threads qui attendent ce moniteur, et ils sont tous en compétition pour la réacquisition du verrou.

L'implémentation suivante deBlockingQueue montre comment plusieurs threads fonctionnent ensemble via le modèlewait-notify. Si nousput un élément dans une file d'attente vide, tous les threads qui attendaient dans la méthodetake se réveillent et essaient de recevoir la valeur. Si nousput un élément dans une file d'attente pleine, la méthodeputwaits pour l'appel à la méthodeget. La méthodeget supprime un élément et notifie les threads en attente dans la méthodeput que la file d'attente a une place vide pour un nouvel élément.

public class BlockingQueue {

    private List queue = new LinkedList();

    private int limit = 10;

    public synchronized void put(T item) {
        while (queue.size() == limit) {
            try {
                wait();
            } catch (InterruptedException e) {}
        }
        if (queue.isEmpty()) {
            notifyAll();
        }
        queue.add(item);
    }

    public synchronized T take() throws InterruptedException {
        while (queue.isEmpty()) {
            try {
                wait();
            } catch (InterruptedException e) {}
        }
        if (queue.size() == limit) {
            notifyAll();
        }
        return queue.remove(0);
    }

}

Q16. Décrivez les conditions de l’impasse, de Livelock et de la famine. Décrivez les causes possibles de ces conditions.

Deadlock est une condition dans un groupe de threads qui ne peut pas progresser car chaque thread du groupe doit acquérir une ressource déjà acquise par un autre thread du groupe. Le cas le plus simple est celui où deux threads doivent verrouiller les deux ressources pour progresser, la première ressource est déjà verrouillée par un thread et la seconde par un autre. Ces threads n'acquerront jamais de verrou sur les deux ressources et ne progresseront donc jamais.

Livelock est le cas de plusieurs threads réagissant à des conditions ou événements générés par eux-mêmes. Un événement se produit dans un thread et doit être traité par un autre thread. Au cours de ce traitement, un nouvel événement se produit qui doit être traité dans le premier thread, etc. De tels fils sont vivants et ne sont pas bloqués, mais ils ne progressent pas car ils se submergent de travail inutile.

Starvation est le cas d'un thread incapable d'acquérir une ressource car d'autres threads (ou threads) l'occupent trop longtemps ou ont une priorité plus élevée. Un fil ne peut pas progresser et ne peut donc pas accomplir un travail utile.

Q17. Décrivez le but et les cas d'utilisation du framework Fork / Join.

Le framework fork / join permet de paralléliser des algorithmes récursifs. Le principal problème avec la parallélisation de la récursivité en utilisant quelque chose commeThreadPoolExecutor est que vous pouvez rapidement manquer de threads car chaque étape récursive nécessiterait son propre thread, tandis que les threads en haut de la pile seraient inactifs et en attente.

Le point d'entrée du framework fork / join est la classeForkJoinPool qui est une implémentation deExecutorService. Il implémente l'algorithme de vol de travail, où les threads inactifs essaient de «voler» le travail de threads occupés. Cela permet de répartir les calculs entre différents threads et de progresser tout en utilisant moins de threads que ce qu’il faudrait avec un pool de threads habituel.

Plus d'informations et d'exemples de code pour le framework fork / join peuvent être trouvés dans l'article“Guide to the Fork/Join Framework in Java”.