JEP 193: Variable Handles
Mit der schon erläuterten späteren Absicht, sun.misc.Unsafe
zu ersetzen, muss für die notwendigen APIs die Unsafe bereitstellt, Ersatz geschaffen werden.
Für den direkten, atomaren Zugriff auf Felder von Objekten, Feldelementen und Speicherbereichen wird diese Aufgabe in Zukunft von VarHandles übernommen.
Dabei sollen die gewünschten Zugriffseffekte in Bezug auf das Java Speichermodell möglich sein, wie zum Beispiel eine volatile oder atomare Schreiboperation.
Anders als bei Unsafe dürfen aber bestimmte Sicherheitsmechanismen nicht ausgehebelt werden, wie z.B. der Zuweisungstypcheck eines Objektfeldes oder die Indexüberprüfung bei Array-Zugriffen.
Ein Ziel bei der Entwicklung war diesselbe Geschwindgkeit wie aber bessere Nutzerfreundlichkeit als für Unsafe.
Der vom Java Compiler generierte Assembler-Code sollte dem von Unsafe genutzen weitgehend entsprechen.
Für ihre Realisierung mussten Erweiterungen im JDK, der JVM und im Compiler vorgenommen werden.
Warum ist das alles notwendig?
Besonders in hochparallelen, perfomance-kritischen Anwendungen wie z.B. Big Data Frameworks und Datenbanken will man mit minimalem Aufwand and Speicher und CPU sichere Programme schreiben.
Dazu sind atomare Operationen auf Feldern eine Grundvoraussetzung.
Man kann das ganze zwar korrekt mit dem Hilfsklassen in java.util.concurrent
erreichen, die CAS benutzen, wie AtomicInteger
oder die verschiedenen FieldUpdater
, aber halt nicht effizient.
Daher wurde in der Vergangenheit meist zu Unsafe gegriffen, um solche Operationen umzusetzen.
In Zukunft sollen das VarHandles übernehmen.
Zum einen werden VarHandles ähnlich wie MethodHandles über MethodHandles.lookup()
ermittelt.
Dann enthält VarHandle (siehe [VarHandle] sehr viele Methoden für Lese-, Schreib-, Bit-, CompareAndSwap-, Additions- und andere Zugriffsoperationen auf die Felder, wobei nicht jede Operation auf jedem Datentyp definiert ist.
Z.B. gibt es keine Modifikationen auf final
Feldern, keine add*
-Operation auf booleschen Feldern, oder zur Zeit noch keine Bitoperationen auf float
und double
.
Jede dieser Methoden hat dokumentierte Speicher-Sichtbarkeitsregeln, wie zum Beispiel volatiler Zugriff (auch ohne dass die Variable als volatile
definiert ist).
Desweiteren gibt es eine Handvoll von statischen Marker-Methoden, wie z.B. loadLoadFence()
, die die erlaubten Operations-Reordering-Regeln abbilden (siehe Java-Memory-Modell Artikel [Hung]).
Hier ist ein einfaches Beispiel:
class Transactions {
int count;
}
VarHandle counter =
MethodHandles.lookup().in(Transactions.class)
.findVarHandle(Transactions.class, "count", int.class);
Transactions tx = new Transactions();
int vol = (int)counter.getAndAdd(tx,1); // 0
counter.get(tx); // 1
counter.compareAndSet(tx,1,2); // true
counter.getVolatile(tx); // 2
Um von den Compiler Optimierungen (Intrinsics, Constant-Folding) zu profitieren, sollte das VarHandle aber in einer static final
Konstante abgelegt werden (und in einem static Initializer Block initialisiert).
Ein interessanter Aspekt ist wie die Implementierung von VarHandles mit einer einzigen abstrakten Klasse vorgenommen werden kann, deren Methodensignatur aus einem Object-varargs und einem Object Rückgabewert bestehen, zum Bespiel: Object VarHandle.getAndAdd(Object…args)
.
Diese Methoden werden "signature-polymorphic" genannt, da sie nicht auf Subklassenvererbung beruhen, sondern rein vom Compiler und der Runtime gehandhabt werden.
Für die Bestimmung der Zieltypen der Methodenparameter werden die Typen der übergebenen Parameter genutzt.
Damit wird die Compilierbarkeit sichergestellt, der Bytecode enthält die Informationen über die genutzen Typ-Parameter. Es wird aber kein Boxing von Objekten vorgenommen, das sieht nur so aus.
Intern wird dann der Methodenaufruf komplett durch die Implementierung der Operation ersetzt, wie bei einer intrinsischen Methode.
Der Rückgabetyp wird noch nicht aus Ziel-Typ der Call-Site inferiert, so dass immer noch ein Type-Cast notwendig ist.
Ein cooles Feature ist die Behandlung eines Speicherbereiches (byte-Array) als die Repräsentation eines anderen Datentyps, z.B. long
oder double
.
Ein Fallstrick ist hier aber die Berechung des Indexes für den Wert, den man selbst mit der Datenbreite (z.b. 8 bytes pro long Wert) inkrementieren muss.
byte[] bytes = new byte[100]
VarHandle view = MethodHandles
.byteArrayViewVarHandle(long[].class, ByteOrder.BIG_ENDIAN);
view.get(bytes,8) // 0
view.set(bytes,8,-1L)
view.get(bytes,8) // -1
view.set(bytes,0,98L)
// byte[10000] { 0, 0, 0, 0, 0, 0, 0, 98, -1, -1, -1, -1, -1, -1, -1, -1, 0, 0, 0
Leider wurde in diesem JEP nicht die Gelegenheit genutzt, ähnlich wie für Methoden-referenzen, eine Feld-Referenz einzuführen, ala Transactions::count
.