Java Concurrency Interview Fragen (Antworten)

Fragen im Vorstellungsgespräch bei Java Concurrency (+ Antworten)

1. Einführung

Parallelität in Java ist eines der komplexesten und fortgeschrittensten Themen, die bei technischen Interviews zur Sprache kommen. Dieser Artikel enthält Antworten auf einige der Interviewfragen zu dem Thema, auf das Sie möglicherweise stoßen.

Q1. Was ist der Unterschied zwischen einem Prozess und einem Thread?

Sowohl Prozesse als auch Threads sind Einheiten der Parallelität, aber sie haben einen grundlegenden Unterschied: Prozesse teilen sich keinen gemeinsamen Speicher, während Threads dies tun.

Aus Sicht des Betriebssystems ist ein Prozess eine unabhängige Software, die in einem eigenen virtuellen Speicherbereich ausgeführt wird. Jedes Multitasking-Betriebssystem (was fast jedes moderne Betriebssystem bedeutet) muss Prozesse im Speicher trennen, damit ein fehlerhafter Prozess nicht alle anderen Prozesse durch Verwürfeln des gemeinsamen Speichers nach unten zieht.

Die Prozesse sind daher in der Regel isoliert und kooperieren über die Interprozesskommunikation, die vom Betriebssystem als eine Art Intermediate-API definiert wird.

Im Gegenteil, ein Thread ist Teil einer Anwendung, die einen gemeinsamen Speicher mit anderen Threads derselben Anwendung teilt. Durch die Verwendung des gemeinsamen Speichers können Sie viel Overhead sparen, die Threads so gestalten, dass sie schneller zusammenarbeiten und Daten zwischen ihnen austauschen.

Q2. Wie können Sie eine Thread-Instanz erstellen und ausführen?

Um eine Instanz eines Threads zu erstellen, haben Sie zwei Möglichkeiten. Übergeben Sie zunächst eineRunnable-Instanz an ihren Konstruktor und rufen Siestart() auf. Runnable ist eine funktionale Schnittstelle, daher kann sie als Lambda-Ausdruck übergeben werden:

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

Thread implementiert auchRunnable. Eine andere Möglichkeit zum Starten eines Threads besteht darin, eine anonyme Unterklasse zu erstellen, dierun()-Methode zu überschreiben und dannstart() aufzurufen:

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

Q3. Beschreiben Sie die verschiedenen Zustände eines Threads und wann die Zustandsübergänge auftreten.

Der Zustand vonThread kann mit der MethodeThread.getState() überprüft werden. Verschiedene Zustände vonThread sind in der AufzählungThread.State beschrieben. Sie sind:

  • NEW - Eine neueThread-Instanz, die noch nicht überThread.start() gestartet wurde

  • RUNNABLE - ein laufender Thread. Es wird als ausführbar bezeichnet, da es zu einem bestimmten Zeitpunkt entweder ausgeführt wird oder auf die nächste Zeitmenge vom Thread-Scheduler wartet. EinNEW-Thread wechselt in denRUNNABLE-Status, wenn SieThread.start() darauf aufrufen

  • BLOCKED - Ein laufender Thread wird blockiert, wenn er einen synchronisierten Abschnitt eingeben muss, dies jedoch nicht kann, da ein anderer Thread den Monitor dieses Abschnitts hält

  • WAITING - Ein Thread wechselt in diesen Zustand, wenn er darauf wartet, dass ein anderer Thread eine bestimmte Aktion ausführt. Beispielsweise tritt ein Thread in diesen Status ein, wenn er die MethodeObject.wait() auf einem Monitor aufruft, den er enthält, oder die MethodeThread.join() auf einem anderen Thread

  • TIMED_WAITING - wie oben, aber ein Thread tritt in diesen Zustand ein, nachdem zeitgesteuerte Versionen vonThread.sleep(),Object.wait(),Thread.join() und einigen anderen Methoden aufgerufen wurden

  • TERMINATED - Ein Thread hat die Ausführung seinerRunnable.run()-Methode abgeschlossen und beendet

Q4. Was ist der Unterschied zwischen der ausführbaren und der aufrufbaren Schnittstelle? Wie werden sie verwendet?

DieRunnable-Schnittstelle verfügt über eine einzelnerun-Methode. Es stellt eine Berechnungseinheit dar, die in einem separaten Thread ausgeführt werden muss. DieRunnable-Schnittstelle erlaubt dieser Methode nicht, Werte zurückzugeben oder ungeprüfte Ausnahmen auszulösen.

DieCallable-Schnittstelle verfügt über eine einzelnecall-S-Methode und repräsentiert eine Aufgabe, die einen Wert hat. Aus diesem Grund gibt diecall-Methode einen Wert zurück. Es kann auch Ausnahmen auslösen. Callable wird im Allgemeinen inExecutorService Instanzen verwendet, um eine asynchrone Task zu starten und dann die zurückgegebeneFuture Instanz aufzurufen, um ihren Wert zu erhalten.

Q5. Was ist ein Daemon-Thread, was sind seine Anwendungsfälle? Wie können Sie einen Daemon-Thread erstellen?

Ein Daemon-Thread ist ein Thread, der das Beenden von JVM nicht verhindert. Wenn alle Nicht-Daemon-Threads beendet sind, gibt die JVM einfach alle verbleibenden Daemon-Threads auf. Daemon-Threads werden normalerweise verwendet, um einige Support- oder Serviceaufgaben für andere Threads auszuführen. Sie sollten jedoch berücksichtigen, dass sie jederzeit abgebrochen werden können.

Um einen Thread als Daemon zu starten, sollten Sie die MethodesetDaemon() verwenden, bevor Siestart() aufrufen:

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

Wenn Sie dies als Teil dermain()-Methode ausführen, wird die Nachricht möglicherweise nicht gedruckt. Dies kann passieren, wenn der Thread vonmain()beendet wird, bevor der Dämon den Punkt erreicht, an dem die Nachricht gedruckt wird. Im Allgemeinen sollten Sie in Daemon-Threads keine E / A-Vorgänge ausführen, da diese nicht einmal in der Lage sind, ihrefinally-Blöcke auszuführen und die Ressourcen zu schließen, wenn sie abgebrochen werden.

Q6. Was ist das Interrupt-Flag des Threads? Wie können Sie es einstellen und prüfen? Wie hängt es mit der Interruptedexception zusammen?

Das Interrupt-Flag oder der Interrupt-Status ist ein internesThread-Flag, das gesetzt wird, wenn der Thread unterbrochen wird. Um dies festzulegen, rufen Sie einfachthread.interrupt() für das Thread-Objekt. auf

Befindet sich ein Thread derzeit in einer der Methoden, dieInterruptedException (wait,join,sleep usw.) auslösen, löst diese Methode sofort eine InterruptedException aus. Der Thread kann diese Ausnahme nach seiner eigenen Logik verarbeiten.

Wenn sich ein Thread nicht in einer solchen Methode befindet undthread.interrupt() aufgerufen wird, passiert nichts Besonderes. Es liegt in der Verantwortung des Threads, den Interrupt-Status regelmäßig mit der Methodestatic Thread.interrupted() oder der InstanzisInterrupted()zu überprüfen. Der Unterschied zwischen diesen Methoden besteht darin, dassstatic Thread.interrupt() das Interrupt-Flag löscht,isInterrupted() jedoch nicht.

Q7. Was sind Executor und Executorservice? Was sind die Unterschiede zwischen diesen Schnittstellen?

Executor undExecutorService sind zwei verwandte Schnittstellen desjava.util.concurrent Frameworks. Executor ist eine sehr einfache Schnittstelle mit einer einzelnenexecute-Methode, dieRunnable Instanzen zur Ausführung akzeptiert. In den meisten Fällen ist dies die Schnittstelle, von der Ihr aufgabenausführender Code abhängen sollte.

ExecutorService erweitert dieExecutor-Schnittstelle um mehrere Methoden zur Behandlung und Überprüfung des Lebenszyklus eines gleichzeitigen Taskausführungsdienstes (Beendigung von Tasks beim Herunterfahren) und Methoden zur komplexeren asynchronen Taskbearbeitung, einschließlichFutures. s.

Weitere Informationen zur Verwendung vonExecutor undExecutorService finden Sie im ArtikelA Guide to Java ExecutorService.

Q8. Was sind die verfügbaren Implementierungen von Executorservice in der Standardbibliothek?

DieExecutorService-Schnittstelle verfügt über drei Standardimplementierungen:

  • ThreadPoolExecutor - zum Ausführen von Aufgaben mithilfe eines Thread-Pools. Sobald ein Thread die Aufgabe ausgeführt hat, wird er wieder in den Pool verschoben. Wenn alle Threads im Pool belegt sind, muss der Task warten, bis er an der Reihe ist.

  • ScheduledThreadPoolExecutor ermöglicht das Planen der Aufgabenausführung, anstatt sie sofort auszuführen, wenn ein Thread verfügbar ist. Es können auch Aufgaben mit fester Rate oder fester Verzögerung geplant werden.

  • ForkJoinPool ist ein speziellesExecutorService für die Bearbeitung von Aufgaben mit rekursiven Algorithmen. Wenn Sie für einen rekursiven Algorithmus ein reguläresThreadPoolExecutor verwenden, werden Sie schnell feststellen, dass alle Ihre Threads damit beschäftigt sind, auf den Abschluss der unteren Rekursionsstufen zu warten. DasForkJoinPool implementiert den sogenannten Work-Stealing-Algorithmus, mit dem verfügbare Threads effizienter genutzt werden können.

Q9. Was ist Java Memory Model (Jmm)? Beschreiben Sie den Zweck und die Grundideen.

Das Java-Speichermodell ist Teil der inChapter 17.4 beschriebenen Java-Sprachspezifikation. Es gibt an, wie mehrere Threads in einer gleichzeitigen Java-Anwendung auf den gemeinsamen Speicher zugreifen und wie Datenänderungen durch einen Thread für andere Threads sichtbar gemacht werden. JMM ist zwar recht kurz und prägnant, kann aber ohne soliden mathematischen Hintergrund schwer zu verstehen sein.

Das Erfordernis eines Speichermodells ergibt sich aus der Tatsache, dass die Art und Weise, wie Ihr Java-Code auf Daten zugreift, nicht der tatsächlichen Vorgehensweise auf den unteren Ebenen entspricht. Schreib- und Lesevorgänge im Speicher können vom Java-Compiler, JIT-Compiler und sogar von der CPU neu angeordnet oder optimiert werden, sofern das beobachtbare Ergebnis dieser Lese- und Schreibvorgänge dasselbe ist.

Dies kann zu nicht intuitiven Ergebnissen führen, wenn Ihre Anwendung auf mehrere Threads skaliert wird, da die meisten dieser Optimierungen einen einzelnen Ausführungsthread berücksichtigen (die Cross-Thread-Optimierer sind immer noch äußerst schwierig zu implementieren). Ein weiteres großes Problem besteht darin, dass der Speicher in modernen Systemen mehrschichtig ist: Mehrere Prozessorkerne speichern möglicherweise nicht gelöschte Daten in ihren Caches oder Lese- / Schreibpuffern, was sich auch auf den von anderen Kernen beobachteten Speicherstatus auswirkt.

Erschwerend kommt hinzu, dass das Vorhandensein unterschiedlicher Speicherzugriffsarchitekturen das Versprechen von Java brechen würde, "einmal schreiben, überall ausführen". Glücklicherweise gibt das JMM einige Garantien vor, auf die Sie sich beim Entwerfen von Multithread-Anwendungen verlassen können. Das Einhalten dieser Garantien hilft einem Programmierer, Multithread-Code zu schreiben, der stabil und zwischen verschiedenen Architekturen portierbar ist.

Die Hauptbegriffe von JMM sind:

  • Actions, dies sind Aktionen zwischen Threads, die von einem Thread ausgeführt und von einem anderen Thread erkannt werden können, z. B. Lesen oder Schreiben von Variablen, Sperren / Entsperren von Monitoren usw.

  • Synchronization actions, eine bestimmte Teilmenge von Aktionen, z. B. Lesen / Schreiben einervolatile-Variablen oder Sperren / Entsperren eines Monitors

  • Program Order (PO), die beobachtbare Gesamtreihenfolge von Aktionen innerhalb eines einzelnen Threads

  • Synchronization Order (SO), die Gesamtreihenfolge zwischen allen Synchronisationsaktionen - muss mit der Programmreihenfolge übereinstimmen, dh wenn zwei Synchronisationsaktionen in PO voreinander liegen, treten sie in SO in derselben Reihenfolge auf

  • synchronizes-with (SW) Beziehung zwischen bestimmten Synchronisationsaktionen, wie dem Entsperren des Monitors und dem Sperren desselben Monitors (in einem anderen oder demselben Thread)

  • Happens-before Order - kombiniert PO mit SW (dies wird in der Mengenlehre alstransitive closure bezeichnet), um eine teilweise Reihenfolge aller Aktionen zwischen Threads zu erstellen. Wenn eine Aktionhappens-beforeeine andere ist, können die Ergebnisse der ersten Aktion von der zweiten Aktion beobachtet werden (z. B. Schreiben einer Variablen in einen Thread und Lesen eines anderen).

  • Happens-before consistency - Eine Reihe von Aktionen ist HB-konsistent, wenn bei jedem Lesevorgang entweder der letzte Schreibvorgang an dieser Stelle in der Reihenfolge vor dem Auftreten oder ein anderer Schreibvorgang über ein Datenrennen beobachtet wird

  • Execution - ein bestimmter Satz geordneter Aktionen und Konsistenzregeln zwischen ihnen

Für ein bestimmtes Programm können wir mehrere verschiedene Ausführungen mit unterschiedlichen Ergebnissen beobachten. Wenn ein Programm jedochcorrectly synchronized ist, scheinen alle seine Ausführungensequentially consistent zu sein, was bedeutet, dass Sie das Multithread-Programm als eine Reihe von Aktionen betrachten können, die in einer bestimmten Reihenfolge ausgeführt werden. Dies erspart Ihnen das Nachdenken über Nachbestellungen, Optimierungen oder das Zwischenspeichern von Daten.

Q10. Was ist ein volatiles Feld und welche Garantien gilt für ein solches Feld?

Das Feldvolatilehat spezielle Eigenschaften gemäß dem Java-Speichermodell (siehe Q9). Das Lesen und Schreiben einervolatile-Variablen sind Synchronisationsaktionen, was bedeutet, dass sie eine Gesamtreihenfolge haben (alle Threads beobachten eine konsistente Reihenfolge dieser Aktionen). Beim Lesen einer flüchtigen Variablen wird garantiert, dass das letzte Schreiben in diese Variable gemäß dieser Reihenfolge erfolgt.

Wenn Sie ein Feld haben, auf das von mehreren Threads aus zugegriffen wird und in das mindestens ein Thread schreibt, sollten Sie in Betracht ziehen, es aufvolatile zu setzen. Andernfalls gibt es eine kleine Garantie dafür, was ein bestimmter Thread aus diesem Feld lesen würde .

Eine weitere Garantie fürvolatile ist die Atomizität beim Schreiben und Lesen von 64-Bit-Werten (long unddouble). Ohne einen flüchtigen Modifikator könnte ein Lesen eines solchen Feldes einen Wert beobachten, der teilweise von einem anderen Thread geschrieben wurde.

Q11. Welche der folgenden Operationen sind atomar?

  • Schreiben in ein Nicht -volatileint;

  • Schreiben involatile int;

  • Schreiben an ein Nicht -volatile long;

  • Schreiben involatile long;

  • Inkrementieren vonvolatile long?

Ein Schreibvorgang in eineint (32-Bit) -Variable ist garantiert atomar, unabhängig davon, ob es sich umvolatile handelt oder nicht. Einelong (64-Bit) -Variable kann in zwei separaten Schritten geschrieben werden, z. B. auf 32-Bit-Architekturen. Daher gibt es standardmäßig keine Atomaritätsgarantie. Wenn Sie jedoch den Modifikatorvolatile angeben, wird garantiert, dass auf eine Variablelongatomar zugegriffen wird.

Die Inkrementierungsoperation wird normalerweise in mehreren Schritten ausgeführt (Abrufen eines Werts, Ändern und Zurückschreiben), sodass niemals garantiert wird, dass sie atomar ist, ob die Variablevolatile ist oder nicht. Wenn Sie ein atomares Inkrement eines Werts implementieren müssen, sollten Sie die KlassenAtomicInteger,AtomicLong usw. verwenden.

Q12. Welche besonderen Garantien gilt das Jmm für die letzten Felder einer Klasse?

JVM garantiert grundsätzlich, dassfinal Felder einer Klasse initialisiert werden, bevor ein Thread das Objekt erfasst. Ohne diese Garantie kann ein Verweis auf ein Objekt veröffentlicht werden, d.h. werden für einen anderen Thread sichtbar, bevor alle Felder dieses Objekts aufgrund von Neuanordnungen oder anderen Optimierungen initialisiert werden. Dies kann zu einem schnellen Zugriff auf diese Felder führen.

Aus diesem Grund sollten Sie beim Erstellen eines unveränderlichen Objekts immer alle Felderfinal festlegen, auch wenn über Getter-Methoden nicht auf sie zugegriffen werden kann.

Q13. Was bedeutet ein synchronisiertes Schlüsselwort in der Definition einer Methode? einer statischen Methode? Vor einem Block?

Das Schlüsselwortsynchronized vor einem Block bedeutet, dass jeder Thread, der in diesen Block eintritt, den Monitor (das Objekt in Klammern) erfassen muss. Wenn der Monitor bereits von einem anderen Thread erfasst wurde, wechselt der vorherige Thread in den StatusBLOCKEDund wartet, bis der Monitor freigegeben wird.

synchronized(object) {
    // ...
}

Die Instanzmethode vonsynchronizedhat dieselbe Semantik, aber die Instanz selbst fungiert als Monitor.

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

Bei einerstatic synchronized-Methode ist der Monitor dasClass-Objekt, das die deklarierende Klasse darstellt.

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

Q14. Wenn zwei Threads gleichzeitig eine synchronisierte Methode für verschiedene Objektinstanzen aufrufen, kann einer dieser Threads blockieren? Was ist, wenn die Methode statisch ist?

Wenn es sich bei der Methode um eine Instanzmethode handelt, fungiert die Instanz als Monitor für die Methode. Zwei Threads, die die Methode auf verschiedenen Instanzen aufrufen, erwerben unterschiedliche Monitore, sodass keiner von ihnen blockiert wird.

Wenn die Methodestatic ist, ist der Monitor das ObjektClass. Für beide Threads ist der Monitor derselbe, sodass einer von ihnen wahrscheinlich blockiert und darauf wartet, dass ein anderer diesynchronized-Methode beendet.

Q15. Was ist der Zweck der Warte-, Benachrichtigungs- und Benachrichtigungsmethoden der Objektklasse?

Ein Thread, dem der Monitor des Objekts gehört (z. B. ein Thread, der einen vom Objekt geschützten Abschnittsynchronized eingegeben hat), kannobject.wait() aufrufen, um den Monitor vorübergehend freizugeben und anderen Threads die Möglichkeit zu geben, den Monitor zu erwerben . Dies kann zum Beispiel geschehen, um auf eine bestimmte Bedingung zu warten.

Wenn ein anderer Thread, der den Monitor erfasst hat, die Bedingung erfüllt, kann erobject.notify() oderobject.notifyAll() aufrufen und den Monitor freigeben. Die Methodenotifyweckt einen einzelnen Thread im Wartezustand, und die MethodenotifyAllweckt alle Threads, die auf diesen Monitor warten, und alle konkurrieren um die erneute Erfassung der Sperre.

Die folgende Implementierung vonBlockingQueuezeigt, wie mehrere Threads über das Muster vonwait-notifyzusammenarbeiten. Wenn wirputein Element in eine leere Warteschlange stellen, werden alle Threads, die in der Methodetakegewartet haben, aktiviert und versuchen, den Wert zu erhalten. Wenn wirputein Element in eine vollständige Warteschlange stellen, wird dieput-Methodewaits für den Aufruf derget-Methode verwendet. Die Methodeget entfernt ein Element und benachrichtigt die in der Methodeput wartenden Threads, dass die Warteschlange einen leeren Platz für ein neues Element hat.

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. Beschreiben Sie die Bedingungen für Deadlock, Livelock und Hunger. Beschreiben Sie die möglichen Ursachen dieser Bedingungen.

Deadlock ist eine Bedingung innerhalb einer Gruppe von Threads, die keine Fortschritte erzielen kann, da jeder Thread in der Gruppe eine Ressource erwerben muss, die bereits von einem anderen Thread in der Gruppe erfasst wurde. Am einfachsten ist es, wenn zwei Threads beide Ressourcen sperren müssen, um fortzufahren. Die erste Ressource wird bereits von einem Thread und die zweite von einem anderen Thread gesperrt. Diese Threads erhalten niemals eine Sperre für beide Ressourcen und werden daher niemals fortgesetzt.

Livelock ist ein Fall, in dem mehrere Threads auf Bedingungen oder Ereignisse reagieren, die von ihnen selbst generiert wurden. Ein Ereignis tritt in einem Thread auf und muss von einem anderen Thread verarbeitet werden. Während dieser Verarbeitung tritt ein neues Ereignis auf, das im ersten Thread verarbeitet werden muss, und so weiter. Solche Threads sind lebendig und nicht blockiert, machen aber trotzdem keine Fortschritte, weil sie sich gegenseitig mit nutzloser Arbeit überfordern.

Starvation ist ein Fall, in dem ein Thread keine Ressource abrufen kann, weil andere Threads (oder Threads) diese zu lange belegen oder eine höhere Priorität haben. Ein Thread kann keine Fortschritte machen und kann daher keine nützliche Arbeit leisten.

Q17. Beschreiben des Zwecks und der Anwendungsfälle des Fork / Join-Frameworks.

Das Fork / Join-Framework ermöglicht die Parallelisierung rekursiver Algorithmen. Das Hauptproblem bei der Parallelisierung der Rekursion mitThreadPoolExecutor besteht darin, dass Ihnen schnell die Threads ausgehen, da für jeden rekursiven Schritt ein eigener Thread erforderlich wäre, während die Threads im Stapel inaktiv wären und warten würden.

Der Einstiegspunkt für das Fork / Join-Framework ist die KlasseForkJoinPool, die eine Implementierung vonExecutorService ist. Es implementiert den Work-Stealing-Algorithmus, bei dem inaktive Threads versuchen, Arbeit von belegten Threads zu „stehlen“. Auf diese Weise können Sie die Berechnungen auf verschiedene Threads verteilen und Fortschritte erzielen, während Sie weniger Threads verwenden, als dies mit einem normalen Thread-Pool erforderlich wäre.

Weitere Informationen und Codebeispiele für das Fork / Join-Framework finden Sie im Artikel“Guide to the Fork/Join Framework in Java”.