-
Da sich mehrere Threads desselben Prozesses denselben Speicherplatz teilen, bringt dies zwar Komfort, aber auch das ernste Problem von Zugriffskonflikten mit sich. Die Java-Sprache bietet einen speziellen Mechanismus zur Lösung dieses Konflikts und verhindert effektiv, dass mehrere Threads gleichzeitig auf dasselbe Datenobjekt zugreifen.
Da wir das private Schlüsselwort verwenden können, um sicherzustellen, dass nur Methoden auf das Datenobjekt zugreifen können, müssen wir nur einen Mechanismus für Methoden vorschlagen. Dieser Mechanismus ist das synchronisierte Schlüsselwort, das zwei Verwendungszwecke umfasst: synchronisierte Methode und synchronisierter Block.
1. synchronisierte Methode: Deklarieren Sie die synchronisierte Methode, indem Sie das synchronisierte Schlüsselwort zur Methodendeklaration hinzufügen. wie:
public synchronisiert void accessVal(int newVal);
Die synchronisierte Methode steuert den Zugriff auf Klassenmitgliedsvariablen: Jede synchronisierte Methode muss die Sperre der Klasseninstanz erhalten, die die Methode aufruft, bevor sie ausgeführt werden kann. Sobald die Methode ausgeführt wird, belegt sie ausschließlich die Klassenmitgliedsvariable. Die Sperre wird erst aufgehoben, wenn sie von dieser Methode zurückgegeben wird. Danach kann der blockierte Thread die Sperre erhalten und wieder in den ausführbaren Zustand wechseln. Dieser Mechanismus stellt sicher, dass für jede Klasseninstanz gleichzeitig höchstens eine ihrer als synchronisiert deklarierten Mitgliedsfunktionen im ausführbaren Zustand ist (da man höchstens die der Klasseninstanz entsprechende Sperre erhalten kann) (beachten Sie diese Anweisung). ! Da es sich um eine Sperre für eine Klasseninstanz handelt, wird für ein Objekt jeweils nur eine synchronisierte Methode von einem Thread ausgeführt. Dadurch werden Zugriffskonflikte von Klassenmitgliedsvariablen effektiv vermieden (solange alle Methoden vorhanden sind). Zugriffsklassen-Mitgliedsvariablen werden als synchronisiert deklariert.
In Java entspricht nicht nur Klasseninstanzen jeder Klasse eine Sperre. Auf diese Weise können wir auch die statischen Mitgliedsfunktionen der Klasse als synchronisiert deklarieren, um ihren Zugriff auf die statischen Mitgliedsvariablen der Klasse zu steuern.
Mängel der synchronisierten Methode: Wenn eine große Methode als synchronisiert deklariert wird, wird die Effizienz stark beeinträchtigt. Wenn die Thread-Klassenmethode run() als synchronisiert deklariert wird, wird sie normalerweise während des gesamten Lebenszyklus des Threads weiter ausgeführt . Führt dazu, dass die Aufrufe aller synchronisierten Methoden dieser Klasse niemals erfolgreich sind. Natürlich können wir dieses Problem lösen, indem wir den Code, der auf Klassenmitgliedsvariablen zugreift, in eine spezielle Methode einfügen, ihn als synchronisiert deklarieren und ihn in der Hauptmethode aufrufen, aber Java bietet uns eine bessere Lösung, nämlich den synchronisierten Block.
2. Synchronisierter Block: Deklarieren Sie den synchronisierten Block über das synchronisierte Schlüsselwort. Die Syntax lautet wie folgt:
synchronisiert(syncObject) {
//Code zum Ermöglichen der Zugriffskontrolle
}
Ein synchronisierter Block ist ein Codeblock, in dem der Code die Sperre des Objekts syncObject erhalten muss (wie bereits erwähnt, kann es sich um eine Klasseninstanz oder eine Klasse handeln), bevor er ausgeführt werden kann. Der spezifische Mechanismus ist der gleiche wie oben erwähnt. Da es auf jeden Codeblock abzielen und das gesperrte Objekt beliebig angeben kann, bietet es eine hohe Flexibilität.
Thread-Blockierung (Synchronisation)
Um Zugriffskonflikte auf gemeinsam genutzte Speicherbereiche zu lösen, hat Java einen Synchronisierungsmechanismus eingeführt. Lassen Sie uns nun den Zugriff mehrerer Threads auf gemeinsam genutzte Ressourcen untersuchen. Offensichtlich reicht der Synchronisierungsmechanismus nicht mehr aus, da die erforderlichen Ressourcen möglicherweise nicht mehr vorhanden sind Um darauf zugreifen zu können, kann es sein, dass mehrere Ressourcen gleichzeitig bereit sind. Um das Problem der Zugriffskontrolle in diesem Fall zu lösen, hat Java die Unterstützung für Blockierungsmechanismen eingeführt.
Unter Blockieren versteht man das Anhalten der Ausführung eines Threads, um auf das Eintreten einer bestimmten Bedingung zu warten (z. B. darauf, dass eine Ressource bereit ist). Studierende, die Betriebssysteme studiert haben, müssen damit vertraut sein. Java bietet eine große Anzahl von Methoden zur Unterstützung des Blockierens. Lassen Sie uns diese einzeln analysieren.
1. Methode „sleep()“: Mit „sleep()“ können Sie einen Zeitraum in Millisekunden als Parameter angeben. Dadurch wechselt der Thread innerhalb der angegebenen Zeit in den Blockierungszustand und kann keine CPU-Zeit erhalten Der Thread wechselt wieder in den ausführbaren Zustand.
Normalerweise wird Sleep() verwendet, wenn darauf gewartet wird, dass eine Ressource bereit ist: Nachdem der Test festgestellt hat, dass die Bedingung nicht erfüllt ist, lassen Sie den Thread für einen bestimmten Zeitraum blockieren und testen Sie ihn dann erneut, bis die Bedingung erfüllt ist.
2. Methoden „suspend()“ und „resume()“ (verursachen leicht einen Deadlock, veraltet): Die beiden Methoden „suspend()“ führen dazu, dass der Thread in einen Blockierungszustand wechselt und nicht automatisch wiederhergestellt wird aufgerufen, kann der Thread wieder in den ausführbaren Zustand wechseln. Üblicherweise werden suspend() und resume() verwendet, wenn auf ein von einem anderen Thread erzeugtes Ergebnis gewartet wird: Nachdem der Test feststellt, dass das Ergebnis nicht erzeugt wurde, wird der Thread blockiert, und nachdem ein anderer Thread das Ergebnis erzeugt hat, wird resume() aufgerufen. um es fortzusetzen.
3. yield()-Methode: yield() bewirkt, dass der Thread die aktuell zugewiesene CPU-Zeit aufgibt, aber nicht dazu führt, dass der Thread blockiert, d. h. der Thread befindet sich noch in einem ausführbaren Zustand und kann erneut CPU-Zeit zugewiesen bekommen jederzeit. Der Effekt des Aufrufs von yield() ist gleichbedeutend damit, dass der Scheduler davon ausgeht, dass der Thread genügend Zeit ausgeführt hat, um zu einem anderen Thread zu wechseln.
4. Methoden „wait()“ und „notify()“: Die beiden Methoden „wait()“ bewirken, dass der Thread in einen Blockierungszustand übergeht. Es gibt zwei Formen, in denen ein Zeitraum in Millisekunden angegeben werden kann Bei anderen Parametern tritt der erstere wieder in den ausführbaren Zustand ein, wenn die entsprechende notify()-Funktion aufgerufen wird oder die angegebene Zeit überschritten wird, und die letztere muss aufgerufen werden, wenn die entsprechende notify()-Funktion aufgerufen wird.
Auf den ersten Blick scheinen sie sich nicht von den Methodenpaaren suspend() und resume() zu unterscheiden, tatsächlich sind sie jedoch völlig unterschiedlich. Der Hauptunterschied besteht darin, dass alle oben beschriebenen Methoden beim Blockieren die belegte Sperre (sofern sie belegt ist) nicht freigeben, während diese umgekehrte Regel das Gegenteil ist.
Die oben genannten Kernunterschiede führen zu einer Reihe detaillierter Unterschiede.
Erstens gehören alle oben beschriebenen Methoden zur Thread-Klasse, dieses Paar gehört jedoch direkt zur Object-Klasse, dh alle Objekte verfügen über dieses Methodenpaar. Das mag auf den ersten Blick unglaublich erscheinen, ist aber in Wirklichkeit ganz natürlich, denn wenn dieses Methodenpaar blockiert, muss die belegte Sperre aufgehoben werden, und die Sperre wird von jedem Objekt besessen Thread zum Blockieren und die Sperre für das Objekt wird aufgehoben. Der Aufruf der notify()-Methode eines beliebigen Objekts führt dazu, dass ein zufällig ausgewählter Thread blockiert wird, indem die wait()-Methode des Objekts aufgerufen wird, um die Blockierung aufzuheben (diese wird jedoch erst ausgeführt, wenn die Sperre erreicht ist).
Zweitens können alle oben beschriebenen Methoden an jedem Ort aufgerufen werden, aber dieses Methodenpaar (wait() und notify()) muss in einer synchronisierten Methode oder einem synchronisierten Block aufgerufen werden. Der Grund ist ebenfalls sehr einfach, nur in einer synchronisierten Methode oder blockieren Nur der aktuelle Thread belegt die Sperre und die Sperre kann aufgehoben werden. Ebenso muss die Sperre für das Objekt, das dieses Methodenpaar aufruft, dem aktuellen Thread gehören, damit die Sperre aufgehoben werden kann. Daher muss das Methodenaufrufpaar in einer synchronisierten Methode oder einem synchronisierten Block platziert werden, dessen gesperrtes Objekt das Objekt ist, das das Methodenpaar aufruft. Wenn diese Bedingung nicht erfüllt ist, obwohl das Programm noch kompiliert werden kann, tritt zur Laufzeit eine IllegalMonitorStateException-Ausnahme auf.
Aufgrund der oben genannten Eigenschaften der Methoden wait() und notify() werden sie häufig zusammen mit synchronisierten Methoden oder Blöcken verwendet. Ein Vergleich mit dem prozessübergreifenden Kommunikationsmechanismus des Betriebssystems zeigt ihre Ähnlichkeiten: Synchronisierte Methoden oder Blöcke bieten Ähnliches Zu den Funktionen von Betriebssystemprimitiven wird ihre Ausführung nicht durch den Multithreading-Mechanismus beeinträchtigt, und dieses Gegenstück entspricht den Block- und Wakeup-Primitiven (dieses Methodenpaar wird beide als synchronisiert deklariert). Ihre Kombination ermöglicht es uns, eine Reihe exquisiter Interprozess-Kommunikationsalgorithmen auf dem Betriebssystem zu implementieren (z. B. Semaphoralgorithmen) und kann zur Lösung verschiedener komplexer Inter-Thread-Kommunikationsprobleme verwendet werden.
Zwei letzte Punkte zu den Methoden wait() und notify():
Erstens: Der durch den Aufruf der notify()-Methode verursachte entsperrte Thread wird zufällig aus den durch den Aufruf der wait()-Methode des Objekts blockierten Threads ausgewählt. Wir können nicht vorhersagen, welcher Thread ausgewählt wird. Seien Sie daher beim Programmieren besonders vorsichtig Probleme, die sich aus dieser Unsicherheit ergeben.
Zweitens: Zusätzlich zu notify() gibt es auch eine Methode notifyAll(), die ebenfalls eine ähnliche Rolle spielen kann. Der einzige Unterschied besteht darin, dass durch Aufrufen der Methode notifyAll() alle Threads entfernt werden, die durch Aufrufen der Methode wait() blockiert werden Objekt auf einmal. Natürlich kann nur der Thread, der die Sperre erhält, in den ausführbaren Zustand wechseln.
Wenn wir über Blockierung sprechen, müssen wir über Deadlock sprechen. Eine kurze Analyse kann zeigen, dass sowohl die Methode suspend() als auch der Aufruf der Methode wait() ohne Angabe einer Zeitüberschreitung einen Deadlock verursachen können. Leider unterstützt Java die Vermeidung von Deadlocks auf Sprachebene nicht, und wir müssen darauf achten, Deadlocks bei der Programmierung zu vermeiden.
Oben haben wir die verschiedenen Methoden zum Blockieren von Threads in Java analysiert. Wir haben uns auf die Methoden wait() und notify() konzentriert, da sie am leistungsstärksten und flexibelsten zu verwenden sind. Dies führt jedoch auch zu einer geringeren Effizienz ist fehleranfälliger. Im tatsächlichen Einsatz sollten wir verschiedene Methoden flexibel einsetzen, um unsere Ziele besser zu erreichen.
Daemon-Thread
Daemon-Threads sind eine besondere Art von Threads. Der Unterschied zwischen ihnen und gewöhnlichen Threads besteht darin, dass sie nicht den Kernteil der Anwendung darstellen. Wenn alle Nicht-Daemon-Threads einer Anwendung beendet werden, wird die Anwendung beendet wird beendet. Andererseits wird die Anwendung nicht beendet, solange ein Nicht-Daemon-Thread ausgeführt wird. Daemon-Threads werden im Allgemeinen verwendet, um anderen Threads im Hintergrund Dienste bereitzustellen.
Sie können feststellen, ob ein Thread ein Daemon-Thread ist, indem Sie die Methode isDaemon() aufrufen, oder Sie können die Methode setDaemon() aufrufen, um einen Thread als Daemon-Thread festzulegen.
Thread-Gruppe
Thread-Gruppe ist ein in Java einzigartiges Konzept. Thread-Gruppe ist ein Objekt der Klasse ThreadGroup. Diese Thread-Gruppe wird beim Erstellen des Threads angegeben und kann nicht während des gesamten Lebenszyklus verwendet werden den Thread ändern. Sie können die Thread-Gruppe angeben, zu der der Thread gehört, indem Sie den Thread-Klassenkonstruktor aufrufen, der einen ThreadGroup-Typparameter enthält. Wenn nicht angegeben, verwendet der Thread standardmäßig die System-Thread-Gruppe mit dem Namen „system“.
In Java müssen alle Thread-Gruppen explizit erstellt werden, mit Ausnahme der vorgefertigten System-Thread-Gruppen. In Java gehört jede Thread-Gruppe mit Ausnahme der System-Thread-Gruppe zu einer anderen Thread-Gruppe. Wenn nicht angegeben, gehört sie standardmäßig zur System-Thread-Gruppe. Auf diese Weise bilden alle Thread-Gruppen einen Baum mit der System-Thread-Gruppe als Wurzel.
Mit Java können wir alle Threads in einer Thread-Gruppe gleichzeitig bearbeiten. Beispielsweise können wir die Priorität aller darin enthaltenen Threads festlegen, indem wir die entsprechende Methode der Thread-Gruppe aufrufen, und wir können auch alle Threads darin starten oder blockieren Es.
Eine weitere wichtige Rolle des Thread-Gruppenmechanismus von Java ist die Thread-Sicherheit. Der Thread-Gruppenmechanismus ermöglicht es uns, Threads mit unterschiedlichen Sicherheitsmerkmalen durch Gruppierung zu unterscheiden, Threads in verschiedenen Gruppen unterschiedlich zu behandeln und die Einführung ungleicher Sicherheitsmaßnahmen durch die hierarchische Struktur von Thread-Gruppen zu unterstützen. Die ThreadGroup-Klasse von Java bietet eine große Anzahl von Methoden, mit denen wir jede Thread-Gruppe im Thread-Gruppenbaum und jeden Thread in der Thread-Gruppe bedienen können.
Thread-Status Zu einem bestimmten Zeitpunkt kann sich ein Thread nur in einem Status befinden.
NEU
Threads, die noch nicht gestartet wurden, befinden sich in diesem Status.
LAUFBAR
Threads, die in der Java Virtual Machine ausgeführt werden, befinden sich in diesem Zustand.
GESPERRT
In diesem Zustand befindet sich ein Thread, der blockiert ist und auf eine Monitorsperre wartet.
WARTEN
In diesem Zustand befindet sich ein Thread, der unbegrenzt darauf wartet, dass ein anderer Thread einen bestimmten Vorgang ausführt.
Der Thread-Status eines wartenden Threads. Ein Thread befindet sich im Wartezustand, weil er eine der folgenden Methoden aufgerufen hat:
Object.wait ohne Timeout-Wert
Thread.join ohne Timeout-Wert
LockSupport.park
Ein Thread im Wartezustand wartet darauf, dass ein anderer Thread einen bestimmten Vorgang ausführt. Beispielsweise wartet ein Thread, der Object.wait() für ein Objekt aufgerufen hat, darauf, dass ein anderer Thread Object.notify() oder Object.notifyAll() für dieses Objekt aufruft. Der Thread, der Thread.join() aufgerufen hat, wartet auf die Beendigung des angegebenen Threads.
TIMED_WAITING
In diesem Zustand befindet sich ein Thread, der darauf wartet, dass ein anderer Thread einen Vorgang ausführt, der von der angegebenen Wartezeit abhängt.
Der Thread-Status eines wartenden Threads mit einer angegebenen Wartezeit. Ein Thread befindet sich in einem zeitgesteuerten Wartezustand, indem er eine der folgenden Methoden mit einer angegebenen positiven Wartezeit aufruft:
Thread.sleep
Object.wait mit Timeout-Wert
Thread.join mit Timeout-Wert
LockSupport.parkNanos
LockSupport.parkUntil
BEENDET
Ein beendeter Thread befindet sich in diesem Zustand.