Parallelität ist sehr wichtig für die Entwicklung robuster, skalierbarer Anwendungen, die mehrere gleichzeitige Vorgänge ausführen können. Allerdings muss hierfür ein Preis in Bezug auf die Synchronisierung gezahlt werden. Aufgrund der damit verbundenen Gemeinkosten für den Erwerb und die Freigabe von Sperren fallen Leistungskosten an. Um diese Leistungseinbußen zu verringern, wurden mehrere Optimierungen in verschiedenen Varianten in die JVM integriert, z. B. voreingenommenes Sperren, Sperrenbeseitigung, Sperrenvergröberung und das Konzept von leichten und schweren Sperren.
In diesem Artikel sehen wir uns diese Optimierungen genauer an und gehen darauf ein, wie sie die Synchronisierung in Multithread-Java-Anwendungen verbessern.
Grundlagen der Java-Sperre
In Java stellt die Synchronisierung von Blöcken oder Methoden sicher, dass jeweils nur ein Thread einen kritischen Codeabschnitt ausführen kann. Dies ist insbesondere dann wichtig, wenn die gemeinsame Nutzung von Ressourcen innerhalb der Multithread-Umgebung in Betracht gezogen wird. Java implementiert dies, indem es sich auf intrinsische Sperren verlässt – manchmal werden sie auch als Monitore bezeichnet, die Objekten oder Klassen zugeordnet sind und dabei helfen, den Zugriff auf die Threads mithilfe der synchronisierten Blöcke zu verwalten.
Obwohl die Synchronisierung eine Notwendigkeit für die Thread-Sicherheit ist, kann sie recht teuer sein, wenn die Konkurrenz gering ist oder überhaupt nicht vorhanden ist. Hier kommen JVM-Optimierungen ins Spiel. Dadurch werden die Sperrkosten gesenkt und die Gesamtleistung verbessert.
1. Voreingenommene Verriegelung
Was ist Biased Locking?
Biased Locking ist eine Optimierung, die auf die Reduzierung des Overheads bei der Sperrenerfassung abzielt. Es ist optimiert, um die Kosten für den Erwerb von Sperren zu reduzieren, die von einem einzelnen Thread dominiert werden oder auf die größtenteils von einem einzelnen Thread zugegriffen wird. Solche Programme erwerben und geben Sperren häufig durch denselben Thread frei, ohne dass andere Threads darauf einwirken. Die JVM kann dieses Muster erkennen und richtet die Sperre auf diesen bestimmten Thread aus. Der folgende Schlosserwerb ist fast kostenlos.
Wie funktioniert Biased Locking?
Wenn die voreingenommene Sperrung aktiviert ist, wird die Sperre, wenn ein Thread zum ersten Mal eine Sperre erhält, auf diesen Thread ausgerichtet. Die Identität des Threads wird im Header des Sperrobjekts aufgezeichnet, und nachfolgende Sperrenerlangungen durch diesen Thread erfordern keinerlei Synchronisierung – sie prüfen lediglich, ob die Sperre auf den aktuellen Thread ausgerichtet ist, was ein sehr schneller, nicht blockierender Vorgang ist .
Wenn ein anderer Thread versucht, die Sperre zu erlangen, wird die Voreingenommenheit aufgehoben und JVM greift auf einen standardmäßigen unvoreingenommenen Sperrmechanismus zurück. Zu diesem Zeitpunkt handelt es sich nun um eine Standardsperre und der zweite Thread muss sie über einen Standardsperrvorgang erwerben.
Vorteile von Biased Locking
Leistung: Der Erwerb desselben Threads auf einer voreingenommenen Sperre ist fast ein kostenloser Sperrenerwerb.
Daher ist keine Konfliktbearbeitung erforderlich, da andere Threads keine Chance haben, an der Erlangung der Sperre beteiligt zu sein.
Geringerer Overhead: Der Status der Sperre muss sich nicht ändern oder synchronisierungsbezogene Metadaten müssen geändert werden, außer im Konfliktfall.
Wann wird Biased Locking verwendet?
Voreingenommenes Sperren ist in Anwendungen nützlich, in denen Sperren hauptsächlich durch denselben Thread zugegriffen werden, z. B. Single-Threaded-Anwendungen oder eine Anwendung, die unter Multithreading nur geringe Sperrenkonflikte aufweist. Es ist in den meisten JVMs standardmäßig aktiviert.
So deaktivieren Sie die voreingenommene Sperrung
Biased Locking ist standardmäßig aktiviert, kann aber auch mit dem JVM-Flag wie unten deaktiviert werden:
-XX:-UseBiasedLocking
2. Sperrenbeseitigung
Was ist Sperrenbeseitigung?
Die Eliminierung von Sperren ist eine sehr leistungsstarke Optimierung, bei der die JVM einige unnötige Synchronisierungen (Sperren) vollständig eliminiert. Während der JIT-Kompilierung wird der Code auf mögliche Möglichkeiten untersucht und dabei festgestellt, dass keine Synchronisierung erforderlich ist. Dies tritt normalerweise auf, wenn nur ein Thread auf die Sperre zugegriffen hat oder das Objekt, das die JVM zum Synchronisieren verwendet, nicht dasselbe Objekt in verschiedenen Threads verwendet. Sobald die JVM feststellt, dass sie nicht mehr erforderlich ist, hebt sie die Sperre auf.
Wie funktioniert die Sperrenbeseitigung?
In der Escape-Analysephase der JIT-Kompilierung prüft JVM, ob das Objekt auf einen einzelnen Thread beschränkt ist oder nur in einem lokalen Kontext verwendet wird. Wenn die Synchronisierung für dieses Objekt entfernt werden kann, weil ein Objekt den Bereich des Threads, der es erstellt hat, nicht verlässt, ist dies der Fall.
Wenn beispielsweise ein Objekt vollständig innerhalb einer Methode erstellt und verwendet wird (und nicht von mehreren Threads gemeinsam genutzt wird), erkennt die JVM, dass möglicherweise kein anderer Thread auf das Objekt zugreifen kann und dass daher die gesamte Synchronisierung redundant ist. In einem solchen Fall eliminiert der JIT-Compiler die Sperre einfach vollständig.
Kein Sperraufwand: Durch die Eliminierung unnötiger Synchronisierung wird auch verhindert, dass die JVM die Kosten für den Erwerb und die Freigabe von Sperren überhaupt trägt.
Höherer Durchsatz:Dead Synch kann manchmal zu einem höheren Durchsatz der Anwendung führen, insbesondere wenn der Code viele synchronisierte Blöcke enthält.
Sehen Sie sich diesen Code an:
public void someMethod() { StringBuilder sb = new StringBuilder(); synchronized (sb) { sb.append("Hello"); sb.append("World"); } }
In diesem Fall ist eine Synchronisierung auf sb nicht erforderlich, da der StringBuilder nur innerhalb der someMethod verwendet und nicht von anderen Threads gemeinsam genutzt wird. Auf diese Weise kann die JVM eine Escape-Analyse durchführen, um die Sperre zu entfernen.
3. Vergröberung sperren
Was ist Lock Coarsening?
Sperrenvergröberung ist eine Optimierung, bei der die JVM den Umfang einer Sperre erweitert, um mehr Codeblöcke abzudecken, anstatt die Sperre kontinuierlich in Schleifen oder kleinen Codeabschnitten zu erfassen und freizugeben.
Vergröberungsarbeiten sperren
Wenn die JVM feststellt, dass eine enge Schleife oder mehrere benachbarte Codeblöcke zu häufig eine Sperre erwerben und freigeben, kann sie die Sperre vergröbern, indem sie die Sperre außerhalb der Schleife oder über mehrere Codeblöcke hinweg verteilt. Dies macht den wiederholten Erwerb und die Freigabe der Sperre kostspielig und ermöglicht es einem Thread, die Sperre für weitere Iterationen beizubehalten.
Codebeispiel: Vergröberung sperren
Bedenken Sie dieses Code-Snippet:
for (int i = 0; i < 1000; i++) { synchronized (lock) { // Do something } }
Die Sperrenvergröberung verschiebt die Sperrenerfassung außerhalb der Schleife, sodass der Thread die Sperre nur einmal erhält:
synchronized (lock) { for (int i = 0; i < 1000; i++) { // Do something } }
Die JVM kann die Leistung erheblich verbessern, indem sie mehr Erfassungen und Freigaben der Sperre vermeidet.
Vergröberungsvorteile sichern
Geringere Freiheit beim Sperren-Overhead: Durch die Vergröberung werden Sperrerfassungen und -freigaben vermieden, insbesondere in Hotspot-Code, wie z. B. Schleifen, die tausende Male wiederholt wurden.
Verbesserte Leistung:
Das Sperren über einen längeren Zeitraum verbessert die Leistung im Vergleich zu dem Szenario, in dem ohne Sperren eine solche Sperre mehrmals erworben und freigegeben würde.
4. Leichte und schwere Schlösser
Die JVM verwendet zwei verschiedene Sperrtechniken, basierend auf dem Grad der Konkurrenz zwischen den Threads. Zu diesen Techniken gehören leichte Schlösser und schwere Schlösser.
Leichte Verriegelung
Lightweight Locking findet statt, wenn keine Konfliktsperre vorhanden ist, was bedeutet, dass nur ein Thread versucht, diese Sperre zu erhalten. In solchen Szenarien optimiert die JVM die Erfassung mithilfe einer CAS-Operation, wenn versucht wird, die Sperre zu erhalten, was ohne schwere Synchronisierung erfolgen kann.
Schwergewichtsverriegelung
Falls mehrere Threads dieselbe Sperre erhalten möchten; Das heißt, es liegt ein Konflikt vor, und die JVM eskaliert diesen zu einer schweren Sperrung. Dies würde das Blockieren von Threads auf Betriebssystemebene und deren Verwaltung mithilfe von Synchronisierungsprimitiven auf Betriebssystemebene beinhalten. Schwere Sperren sind langsamer, da sie tatsächlich erfordern, dass das Betriebssystem den Kontextwechsel durchführt und Threads verwaltet.
Sperreneskalation
Wenn bei einer Lightweight-Sperre ein Konflikt auftritt, kann die JVM ihn zu einer Heavyweight-Sperre eskalieren. Eskalation bedeutet hier den Wechsel von der schnellen Sperre auf Benutzerebene zu einer teureren Sperre auf Betriebssystemebene, die Thread-Blockierung umfasst.
Vorteile von Leichtbauschlössern
Schnelle Erfassung einer Sperre: Wenn kein Konflikt besteht, sind Lightweight-Sperren viel schneller als Heavyweight-Sperren, da sie eine Synchronisierung auf Betriebssystemebene vermeiden.
Reduzierte Blockierung: Ohne Konflikte blockieren Threads nicht und nehmen linear mit geringerer Latenz zu.
Nachteile von Schwergewichtsschlössern
Leistungsaufwand: Schwere Sperren verursachen Kosten für Thread-Blockierung, Kontextwechsel und das Aufwecken von Threads mit Leistungseinbußen bei sehr hohen Konfliktregimen.
All diese Optimierungen helfen der JVM, die Leistung in Multithread-Anwendungen zu verbessern, sodass Entwickler jetzt sicheren, gleichzeitigen Code schreiben können, ohne große Einbußen beim Synchronisierungsaufwand hinnehmen zu müssen. Das Verständnis dieser Optimierungen kann Entwicklern dabei helfen, effizientere Systeme zu entwerfen, insbesondere in Fällen, in denen das Sperren einen hohen Leistungsnachteil mit sich bringt.
Das obige ist der detaillierte Inhalt vonGrundlegendes zu JVM-Sperroptimierungen. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!