Fragen zum Java-Multithreading-Interview
Ein Prozess ist eine in sich geschlossene Betriebsumgebung, die als Programm oder Anwendung betrachtet werden kann. Ein Thread ist eine Aufgabe, die in einem Prozess ausgeführt wird. Die Java-Laufzeitumgebung ist ein einzelner Prozess, der verschiedene Klassen und Programme enthält. Threads können als leichte Prozesse bezeichnet werden. Threads erfordern weniger Ressourcen zum Erstellen und Verweilen in einem Prozess und können Ressourcen innerhalb des Prozesses gemeinsam nutzen.
In einem Multithread-Programm werden mehrere Threads gleichzeitig ausgeführt, um die Effizienz des Programms zu verbessern. Die CPU wechselt nicht in den Ruhezustand, da ein Thread auf Ressourcen warten muss. Mehrere Threads teilen sich den Heap-Speicher, daher ist es besser, mehrere Threads zu erstellen, um einige Aufgaben auszuführen, als mehrere Prozesse zu erstellen. Beispielsweise sind Servlets besser als CGI, da Servlets Multithreading unterstützen, CGI jedoch nicht.
Wenn wir in einem Java-Programm einen Thread erstellen, wird dieser als Benutzer-Thread bezeichnet. Ein Daemon-Thread ist ein Thread, der im Hintergrund ausgeführt wird und die Beendigung der JVM nicht verhindert. Wenn keine Benutzerthreads ausgeführt werden, schließt die JVM das Programm und wird beendet. Von einem Daemon-Thread erstellte untergeordnete Threads sind immer noch Daemon-Threads.
Es gibt zwei Möglichkeiten, einen Thread zu erstellen: Die eine besteht darin, die Runnable-Schnittstelle zu implementieren und sie dann an den Thread-Konstruktor zu übergeben, um ein Thread-Objekt zu erstellen. Die andere besteht darin, die Thread-Klasse direkt zu erben. Wenn Sie mehr wissen möchten, können Sie diesen Artikel zum Erstellen von Threads in Java lesen.
Wenn wir in einem Java-Programm einen neuen Thread erstellen, ist sein Status „Neu“. Wenn wir die start()-Methode des Threads aufrufen, wird der Status in Runnable geändert. Der Thread-Scheduler weist den Threads im ausführbaren Thread-Pool CPU-Zeit zu und ändert ihren Status in „Wird ausgeführt“. Andere Thread-Status sind „Wartend“, „Blockiert“ und „Tot“. Lesen Sie diesen Artikel, um mehr über den Thread-Lebenszyklus zu erfahren.
Natürlich, aber wenn wir die Methode run() von Thread aufrufen, verhält sie sich wie eine normale Methode. Um unseren Code in einem neuen Thread auszuführen, müssen wir die Methode Thread.start() verwenden.
Wir können die Sleep()-Methode der Thread-Klasse verwenden, um den Thread für einen bestimmten Zeitraum anzuhalten. Es ist zu beachten, dass der Thread dadurch nicht beendet wird, sobald der Thread aus dem Ruhezustand aufgeweckt wird. Der Status des Threads wird in „Ausführbar“ geändert und er wird gemäß dem Thread-Zeitplan ausgeführt.
Jeder Thread hat eine Priorität. Im Allgemeinen haben Threads mit hoher Priorität Priorität, wenn sie ausgeführt werden. Dies hängt jedoch von der Implementierung der Thread-Planung ab, die vom Betriebssystem abhängt. Wir können die Priorität von Threads definieren, aber das garantiert nicht, dass Threads mit hoher Priorität vor Threads mit niedriger Priorität ausgeführt werden. Die Thread-Priorität ist eine int-Variable (von 1 bis 10), 1 steht für die niedrigste Priorität, 10 für die höchste Priorität.
Der Thread-Scheduler ist ein Betriebssystemdienst, der für die Zuweisung von CPU-Zeit an Threads im ausführbaren Zustand verantwortlich ist. Sobald wir einen Thread erstellt und gestartet haben, hängt seine Ausführung von der Implementierung des Thread-Schedulers ab. Time-Slicing bezieht sich auf den Prozess der Zuweisung verfügbarer CPU-Zeit zu verfügbaren ausführbaren Threads. Die Zuweisung von CPU-Zeit kann auf der Thread-Priorität oder der Wartezeit des Threads basieren. Die Thread-Planung wird nicht von der Java Virtual Machine gesteuert, daher ist es besser, sie von der Anwendung zu steuern (d. h. Ihr Programm nicht von der Thread-Priorität abhängig zu machen).
Beim Kontextwechsel handelt es sich um den Prozess des Speicherns und Wiederherstellens des CPU-Status, der es der Thread-Ausführung ermöglicht, die Ausführung am Punkt der Unterbrechung fortzusetzen. Kontextwechsel ist ein wesentliches Merkmal von Multitasking-Betriebssystemen und Multithread-Umgebungen.
Wir können die Methode joint() der Thread-Klasse verwenden, um sicherzustellen, dass alle vom Programm erstellten Threads enden, bevor die Methode main() beendet wird. Hier ist ein Artikel über die joint()-Methode der Thread-Klasse.
Wenn Ressourcen zwischen Threads gemeinsam genutzt werden können, ist die Kommunikation zwischen Threads ein wichtiges Mittel zu deren Koordinierung. Die Methoden wait()/notify()/notifyAll() in der Object-Klasse können verwendet werden, um zwischen Threads über den Status von Ressourcensperren zu kommunizieren. Klicken Sie hier, um mehr über Thread Wait, Notify und NotifyAll zu erfahren.
Jedes Objekt in Java verfügt über eine Sperre (Monitor, der auch ein Monitor sein kann), und Methoden wie wait () und notify () werden verwendet, um auf die Sperre des Objekts zu warten oder andere Threads darüber zu informieren, dass der Monitor des Objekts verfügbar ist. Für kein Objekt in Java-Threads sind Sperren oder Synchronisierer verfügbar. Aus diesem Grund sind diese Methoden Teil der Object-Klasse, sodass jede Klasse in Java über grundlegende Methoden für die Kommunikation zwischen Threads verfügt
Wenn ein Thread die wait()-Methode eines Objekts aufrufen muss, muss der Thread die Sperre des Objekts besitzen. Dann wird er die Objektsperre aufheben und in den Wartezustand wechseln, bis andere Threads die notify()-Methode für das Objekt aufrufen. Wenn ein Thread die notify()-Methode des Objekts aufrufen muss, gibt er in ähnlicher Weise die Sperre des Objekts frei, sodass andere wartende Threads die Objektsperre erhalten können. Da alle diese Methoden erfordern, dass der Thread die Sperre des Objekts hält, was nur durch Synchronisierung erreicht werden kann, können sie nur in synchronisierten Methoden oder synchronisierten Blöcken aufgerufen werden.
Die Methoden sleep() und yield() der Thread-Klasse werden auf dem aktuell ausgeführten Thread ausgeführt. Es macht also keinen Sinn, diese Methoden in anderen wartenden Threads aufzurufen. Deshalb sind diese Methoden statisch. Sie können im aktuell ausgeführten Thread arbeiten und verhindern, dass Programmierer fälschlicherweise denken, dass diese Methoden in anderen nicht laufenden Threads aufgerufen werden können.
Es gibt viele Möglichkeiten, die Thread-Sicherheit in Java sicherzustellen: Synchronisierung, Verwendung atomarer gleichzeitiger Klassen, Implementierung gleichzeitiger Sperren, Verwendung des Schlüsselworts volatile, Verwendung unveränderlicher Klassen und threadsicherer Klassen. Weitere Informationen finden Sie im Thread-Sicherheits-Tutorial.
Wenn wir das Schlüsselwort volatile verwenden, um eine Variable zu ändern, liest der Thread die Variable direkt und speichert sie nicht zwischen. Dadurch wird sichergestellt, dass die vom Thread gelesenen Variablen mit denen im Speicher übereinstimmen.
Ein synchronisierter Block ist die bessere Wahl, da er nicht das gesamte Objekt sperrt (Sie können natürlich auch festlegen, dass er das gesamte Objekt sperrt). Synchronisierte Methoden sperren das gesamte Objekt, auch wenn die Klasse mehrere unabhängige synchronisierte Blöcke enthält, was normalerweise dazu führt, dass sie nicht mehr ausgeführt werden und warten müssen, bis die Sperre für das Objekt erhalten wird.
Der Thread kann mithilfe der setDaemon(true)-Methode der Thread-Klasse als Daemon-Thread festgelegt werden. Beachten Sie, dass diese Methode vor dem Aufruf der start()-Methode aufgerufen werden muss, andernfalls wird eine IllegalThreadStateException ausgelöst.
ThreadLocal wird zum Erstellen lokaler Thread-Variablen verwendet. Wir wissen, dass alle Threads eines Objekts seine globalen Variablen gemeinsam nutzen, sodass diese Variablen nicht threadsicher sind. Wir können die Synchronisierungstechnologie verwenden. Wenn wir jedoch keine Synchronisierung verwenden möchten, können wir ThreadLocal-Variablen auswählen.
Jeder Thread hat seine eigenen Thread-Variablen und kann die Methoden get()/set() verwenden, um seine Standardwerte abzurufen oder seine Werte innerhalb des Threads zu ändern. ThreadLocal-Instanzen möchten normalerweise, dass der zugehörige Thread-Status private statische Eigenschaften ist. Im ThreadLocal-Beispielartikel sehen Sie ein kleines Programm zu ThreadLocal.
ThreadGroup ist eine Klasse, deren Zweck darin besteht, Informationen über Thread-Gruppen bereitzustellen.
Die ThreadGroup-API ist relativ schwach und bietet nicht mehr Funktionen als Thread. Es hat zwei Hauptfunktionen: Die eine besteht darin, die Liste der aktiven Threads in der Thread-Gruppe abzurufen, die andere darin, den nicht abgefangenen Ausnahmehandler (ncaught Exception handler) für den Thread festzulegen. In Java 1.5 hat die Thread-Klasse jedoch auch die Methode setUncaughtExceptionHandler(UncaughtExceptionHandler eh) hinzugefügt, sodass ThreadGroup veraltet ist und die weitere Verwendung nicht empfohlen wird.
t1.setUncaughtExceptionHandler(new UncaughtExceptionHandler(){ @Overridepublic void uncaughtException(Thread t, Throwable e) {System.out.println("Exception aufgetreten:"+e.getMessage());} });
Ein Thread-Dump ist eine Liste aktiver JVM-Threads, die für die Analyse von Systemengpässen und Deadlocks sehr nützlich ist. Es gibt viele Möglichkeiten, Thread-Dumps zu erhalten – mit Profiler, dem Befehl Kill -3, dem Jstack-Tool usw. Ich bevorzuge das Jstack-Tool, weil es einfach zu verwenden ist und mit JDK geliefert wird. Da es sich um ein Terminal-basiertes Tool handelt, können wir einige Skripte schreiben, um regelmäßig Thread-Dumps zur Analyse zu generieren. Lesen Sie dieses Dokument, um mehr über das Generieren von Thread-Dumps zu erfahren.
Deadlock bezieht sich auf eine Situation, in der mehr als zwei Threads dauerhaft blockiert sind. Diese Situation erfordert mindestens zwei weitere Threads und mehr als zwei Ressourcen.
Um den Deadlock zu analysieren, müssen wir uns den Thread-Dump der Java-Anwendung ansehen. Wir müssen herausfinden, welche Threads sich im Status BLOCKED befinden und auf welche Ressourcen sie warten. Jede Ressource hat eine eindeutige ID. Mithilfe dieser ID können wir herausfinden, welche Threads ihre Objektsperre bereits besitzen.
Das Vermeiden verschachtelter Sperren, die Verwendung von Sperren nur dort, wo sie benötigt werden, und das Vermeiden unbegrenzter Wartezeiten sind gängige Methoden zur Vermeidung von Deadlocks. Lesen Sie diesen Artikel, um zu erfahren, wie Sie Deadlocks analysieren.
java.util.Timer ist eine Toolklasse, mit der die Ausführung eines Threads zu einem bestimmten Zeitpunkt in der Zukunft geplant werden kann. Mit der Timer-Klasse können einmalige oder periodische Aufgaben geplant werden.
java.util.TimerTask ist eine abstrakte Klasse, die die Runnable-Schnittstelle implementiert. Wir müssen diese Klasse erben, um unsere eigenen geplanten Aufgaben zu erstellen und Timer zum Planen ihrer Ausführung zu verwenden.
Hier finden Sie Beispiele zum Java-Timer.
Ein Thread-Pool verwaltet eine Gruppe von Arbeitsthreads und enthält außerdem eine Warteschlange zum Platzieren von Aufgaben, die auf ihre Ausführung warten.
java.util.concurrent.Executors bietet eine Implementierung der java.util.concurrent.Executor-Schnittstelle zum Erstellen von Thread-Pools. Das Thread-Pool-Beispiel zeigt, wie Sie einen Thread-Pool erstellen und verwenden, oder lesen Sie das ScheduledThreadPoolExecutor-Beispiel, um zu erfahren, wie Sie eine periodische Aufgabe erstellen.
Fragen zum Java-Parallelitätsinterview
Eine atomare Operation bezieht sich auf eine Operationsaufgabeneinheit, die nicht von anderen Operationen beeinflusst wird. Atomare Operationen sind ein notwendiges Mittel, um Dateninkonsistenzen in einer Multithread-Umgebung zu vermeiden.
int++ ist keine atomare Operation. Wenn also ein Thread seinen Wert liest und 1 hinzufügt, liest ein anderer Thread möglicherweise den vorherigen Wert, was zu einem Fehler führt.
Um dieses Problem zu lösen, müssen wir sicherstellen, dass die Erhöhungsoperation atomar ist. Vor JDK1.5 konnten wir hierfür die Synchronisationstechnologie verwenden. Ab JDK 1.5 stellt das Paket java.util.concurrent.atomic Ladeklassen vom Typ int und long bereit, die automatisch garantieren, dass ihre Vorgänge atomar sind und keine Synchronisierung erfordern. Sie können diesen Artikel lesen, um mehr über die Atomklassen von Java zu erfahren.
Die Lock-Schnittstelle bietet skalierbarere Sperrvorgänge als synchronisierte Methoden und synchronisierte Blöcke. Sie ermöglichen flexiblere Strukturen, die völlig unterschiedliche Eigenschaften haben können und mehrere verwandte Klassen bedingter Objekte unterstützen können.
Seine Vorteile sind:
Lesen Sie mehr über Schlossbeispiele
Das Executor-Framework wurde in Java 5 mit der java.util.concurrent.Executor-Schnittstelle eingeführt. Das Executor-Framework ist ein Framework für asynchrone Aufgaben, die gemäß einer Reihe von Ausführungsstrategien aufgerufen, geplant, ausgeführt und gesteuert werden.
Die unbegrenzte Thread-Erstellung kann zu einem Überlauf des Anwendungsspeichers führen. Daher ist die Erstellung eines Thread-Pools eine bessere Lösung, da die Anzahl der Threads begrenzt werden kann und diese Threads recycelt und wiederverwendet werden können. Es ist sehr praktisch, einen Thread-Pool mit dem Executors-Framework zu erstellen. Lesen Sie diesen Artikel, um zu erfahren, wie Sie einen Thread-Pool mit dem Executors-Framework erstellen.
Die Merkmale von java.util.concurrent.BlockingQueue sind: Wenn die Warteschlange leer ist, wird der Vorgang zum Abrufen oder Löschen von Elementen aus der Warteschlange blockiert, oder wenn die Warteschlange voll ist, wird der Vorgang zum Hinzufügen von Elementen zur Warteschlange blockiert .
Die blockierende Warteschlange akzeptiert keine Nullwerte. Wenn Sie versuchen, der Warteschlange einen Nullwert hinzuzufügen, wird eine NullPointerException ausgelöst.
Implementierungen blockierender Warteschlangen sind threadsicher, und alle Abfragemethoden sind atomar und verwenden interne Sperren oder andere Formen der Parallelitätskontrolle.
Die BlockingQueue-Schnittstelle ist Teil des Java-Collections-Frameworks und wird hauptsächlich zur Implementierung des Producer-Consumer-Problems verwendet.
Lesen Sie diesen Artikel, um zu erfahren, wie Sie das Producer-Consumer-Problem mithilfe blockierender Warteschlangen implementieren.
Mit Java 5 wurde die Schnittstelle java.util.concurrent.Callable im Concurrency-Paket eingeführt, die der Runnable-Schnittstelle sehr ähnlich ist, aber ein Objekt zurückgeben oder eine Ausnahme auslösen kann.
Die Callable-Schnittstelle verwendet Generika, um ihren Rückgabetyp zu definieren. Die Executors-Klasse bietet einige nützliche Methoden zum Ausführen von Aufgaben innerhalb von Callable im Thread-Pool. Da die Callable-Aufgabe parallel ist, müssen wir auf das zurückgegebene Ergebnis warten. Das java.util.concurrent.Future-Objekt löst dieses Problem für uns. Nachdem der Thread-Pool die Callable-Aufgabe übermittelt hat, wird ein Future-Objekt zurückgegeben. Mithilfe dieses Objekts können wir den Status der Callable-Aufgabe ermitteln und das vom Callable zurückgegebene Ausführungsergebnis abrufen. Future stellt die Methode get() bereit, damit wir auf das Ende des Callable warten und dessen Ausführungsergebnisse erhalten können.
Lesen Sie diesen Artikel, um weitere Beispiele zu Callable, Future zu erfahren.
FutureTask ist eine grundlegende Implementierung von Future, die wir mit Executors verwenden können, um asynchrone Aufgaben zu verarbeiten. Normalerweise müssen wir die FutureTask-Klasse nicht verwenden, aber sie ist sehr nützlich, wenn wir planen, einige Methoden der Future-Schnittstelle zu überschreiben und die ursprüngliche Basisimplementierung beizubehalten. Wir können einfach davon erben und die Methoden, die wir benötigen, überschreiben. Lesen Sie das Java FutureTask-Beispiel, um zu erfahren, wie Sie es verwenden.
Java-Sammlungsklassen sind ausfallsicher. Das heißt, wenn die Sammlung geändert wird und ein Thread einen Iterator zum Durchlaufen der Sammlung verwendet, löst die next()-Methode des Iterators eine ConcurrentModificationException-Ausnahme aus.
Gleichzeitige Container unterstützen gleichzeitiges Durchlaufen und gleichzeitige Aktualisierungen.
Die Hauptklassen sind ConcurrentHashMap, CopyOnWriteArrayList und CopyOnWriteArraySet. Lesen Sie diesen Artikel, um zu erfahren, wie Sie ConcurrentModificationException vermeiden.
Executoren stellen einige Hilfsmethoden für die Klassen Executor, ExecutorService, ScheduledExecutorService, ThreadFactory und Callable bereit.
Mit Executoren lassen sich ganz einfach Thread-Pools erstellen.
Originaltext: journaldev.com Übersetzung: ifeve Übersetzer: Zheng Xudong