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 ganz fehlt. 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 der Sperrenerfassung abzielt. Es ist optimiert, um die Kosten für den Erwerb von Sperren zu senken, 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.
For example, if an object is created and used entirely within a method (and not shared across threads) the JVM realizes no other thread can possibly access the object, and thus that all synchronization is redundant. In such a case, the JIT compiler simply eliminates the lock altogether.
Zero Locking Overhead: Eliminating unnecessary synchronization will also prevent the JVM from paying the cost of acquiring and releasing locks in the first place.
Higher Throughput: Dead synch can sometimes lead to a higher throughput of the application, especially if the code contains many synchronized blocks.
Take a look at this piece of code:
public void someMethod() { StringBuilder sb = new StringBuilder(); synchronized (sb) { sb.append("Hello"); sb.append("World"); } }
In this case, synchronization on sb is not necessary since the StringBuilder is used only within the someMethod and is not shared between other threads. By looking at this, the JVM can perform an escape analysis to remove the lock.
3. Lock Coarsening
What is Lock Coarsening?
Lock coarsening is an optimization wherein the JVM expands the scope of a lock to cover more chunks of code instead of continuously acquiring and releasing the lock in loops or small sections of code.
Lock Coarsening Work
If the JVM finds that a tight loop or multiple adjacent code blocks acquire and release a lock too frequently, it can coarsen the lock by taking the lock outside the loop or across several blocks of code. This makes repeated acquisition and release of the lockless expensive and enables a thread to hold the lock for more iterations.
Code Example: Lock Coarsening
Consider this code snippet:
for (int i = 0; i < 1000; i++) { synchronized (lock) { // Do something } }
Lock coarsening pushes the lock acquisition outside the loop, so the thread acquires the lock only once:
synchronized (lock) { for (int i = 0; i < 1000; i++) { // Do something } }
The JVM can dramatically improve performance by avoiding more acquires and releases of the lock.
Lock Coarsening Benefits
Less Freedom of Locking Overheads: Coarsening avoids lock acquisitions and releases, especially in hotspot code, such as loops that have been iterated thousands of times.
Improved Performance:
Locking for a longer period improves performance when compared to the scenario in which, without locking, such a lock would be acquired and released multiple times.
4. Light Weight and Heavy Weight Locks
The JVM uses two different locking techniques based on the degree of contention among the threads. Such techniques include lightweight locks and heavyweight locks.
Light Weight Locking
Lightweight locking takes place in the absence of a contention lock, meaning only one thread is trying to acquire that lock. In such scenarios, the JVM optimizes the acquisition using a CAS operation when trying to acquire the lock, which can happen without heavyweight synchronization.
Heavyweight Locking
In case multiple threads want to obtain the same lock; that is, there is contention, the JVM escalates this to heavyweight locking. This would involve blocking threads at the OS level and managing them using OS-level synchronization primitives. Heavyweight locks are slower because they actually require the OS to perform context switching, as well as to manage threads.
Lock Escalation
If contention arises at a lightweight lock, the JVM may escalate it to a heavyweight lock. Escalation here means switching from the fast, user-level lock to a more expensive, OS-level lock which includes thread blocking.
Benefits of Lightweight Locks
Rapid acquisition of a lock: When there is no contention, lightweight locks are far quicker than heavyweight locks because they avoid OS-level synchronization.
Reduced blocking: With no contentions, threads do not block and increase linearly with lower latency.
Disadvantages of Heavyweight Locks
Performance Overhead: Heavyweight locks incur the cost of thread blocking, context switching, and waking up threads with performance degradation at very high contention regimes.
All these optimizations help the JVM improve performance in multi-threaded applications, so developers can now write safe, concurrent code without sacrificing much in the way of synchronization overhead. Understanding these optimizations can help developers design more efficient systems, especially in cases that have a high-performance penalty for locking.
The above is the detailed content of Understanding JVM Lock Optimizations. For more information, please follow other related articles on the PHP Chinese website!