Leitfaden zu sun.misc.Unsafe

1. Übersicht

In diesem Artikel werfen wir einen Blick auf eine faszinierende Klasse, die von JRE - Unsafe aus dem sun.misc- Paket angeboten wird. Diese Klasse bietet uns Mechanismen auf niedriger Ebene, die nur von der Java-Kernbibliothek und nicht von Standardbenutzern verwendet werden sollen.

Dies bietet uns Mechanismen auf niedriger Ebene, die hauptsächlich für den internen Gebrauch innerhalb der Kernbibliotheken entwickelt wurden.

2. Erhalten einer Instanz des Unsicheren

Um die Unsafe- Klasse verwenden zu können, benötigen wir zunächst eine Instanz. Dies ist nicht einfach, da die Klasse nur für den internen Gebrauch konzipiert wurde.

Der Weg zum Abrufen der Instanz erfolgt über die statische Methode getUnsafe (). Die Einschränkung ist, dass standardmäßig eine SecurityException ausgelöst wird .

Glücklicherweise können wir die Instanz mithilfe von Reflection erhalten:

Field f = Unsafe.class.getDeclaredField("theUnsafe"); f.setAccessible(true); unsafe = (Unsafe) f.get(null);

3. Instanziieren einer Klasse mit Unsicher

Angenommen, wir haben eine einfache Klasse mit einem Konstruktor, der beim Erstellen des Objekts einen Variablenwert festlegt:

class InitializationOrdering { private long a; public InitializationOrdering() { this.a = 1; } public long getA() { return this.a; } }

Wenn wir dieses Objekt mit dem Konstruktor initialisieren, gibt die Methode getA () den Wert 1 zurück:

InitializationOrdering o1 = new InitializationOrdering(); assertEquals(o1.getA(), 1);

Wir können jedoch die allocateInstance () -Methode mit Unsafe verwenden. Es wird nur den Speicher für unsere Klasse zuweisen und keinen Konstruktor aufrufen:

InitializationOrdering o3 = (InitializationOrdering) unsafe.allocateInstance(InitializationOrdering.class); assertEquals(o3.getA(), 0);

Beachten Sie, dass der Konstruktor nicht aufgerufen wurde und die getA () -Methode aus diesem Grund den Standardwert für den langen Typ zurückgegeben hat - nämlich 0.

4. Ändern privater Felder

Nehmen wir an, wir haben eine Klasse, die einen geheimen privaten Wert hat:

class SecretHolder { private int SECRET_VALUE = 0; public boolean secretIsDisclosed() { return SECRET_VALUE == 1; } }

Mit der putInt () -Methode von Unsafe können wir einen Wert des privaten SECRET_VALUE- Felds ändern und den Status dieser Instanz ändern / beschädigen :

SecretHolder secretHolder = new SecretHolder(); Field f = secretHolder.getClass().getDeclaredField("SECRET_VALUE"); unsafe.putInt(secretHolder, unsafe.objectFieldOffset(f), 1); assertTrue(secretHolder.secretIsDisclosed());

Sobald wir durch den Reflection-Aufruf ein Feld erhalten, können wir seinen Wert mit Unsafe in einen anderen int- Wert ändern .

5. Eine Ausnahme auslösen

Der Code, der über Unsafe aufgerufen wird, wird vom Compiler nicht auf die gleiche Weise geprüft wie normaler Java-Code. Wir können die throwException () -Methode verwenden, um eine Ausnahme auszulösen, ohne den Aufrufer einzuschränken, diese Ausnahme zu behandeln, selbst wenn es sich um eine aktivierte Ausnahme handelt:

@Test(expected = IOException.class) public void givenUnsafeThrowException_whenThrowCheckedException_thenNotNeedToCatchIt() { unsafe.throwException(new IOException()); }

Nach dem Auslösen einer IOException, die überprüft wird, müssen wir sie weder abfangen noch in der Methodendeklaration angeben.

6. Off-Heap-Speicher

Wenn in einer Anwendung nicht genügend Speicher in der JVM verfügbar ist, wird der GC-Prozess möglicherweise zu häufig ausgeführt. Idealerweise möchten wir einen speziellen Speicherbereich außerhalb des Heaps, der nicht vom GC-Prozess gesteuert wird.

Die allocateMemory () -Methode aus der Unsafe- Klasse gibt uns die Möglichkeit, große Objekte vom Heap zuzuweisen, was bedeutet, dass dieser Speicher vom GC und der JVM nicht gesehen und berücksichtigt wird .

Dies kann sehr nützlich sein, aber wir müssen uns daran erinnern, dass dieser Speicher manuell verwaltet und mit freeMemory () ordnungsgemäß zurückgefordert werden muss, wenn er nicht mehr benötigt wird.

Angenommen, wir möchten das große Off-Heap-Speicherarray von Bytes erstellen. Wir können die allocateMemory () -Methode verwenden, um dies zu erreichen:

class OffHeapArray { private final static int BYTE = 1; private long size; private long address; public OffHeapArray(long size) throws NoSuchFieldException, IllegalAccessException { this.size = size; address = getUnsafe().allocateMemory(size * BYTE); } private Unsafe getUnsafe() throws IllegalAccessException, NoSuchFieldException { Field f = Unsafe.class.getDeclaredField("theUnsafe"); f.setAccessible(true); return (Unsafe) f.get(null); } public void set(long i, byte value) throws NoSuchFieldException, IllegalAccessException { getUnsafe().putByte(address + i * BYTE, value); } public int get(long idx) throws NoSuchFieldException, IllegalAccessException { return getUnsafe().getByte(address + idx * BYTE); } public long size() { return size; } public void freeMemory() throws NoSuchFieldException, IllegalAccessException { getUnsafe().freeMemory(address); }
}

Im Konstruktor des OffHeapArray initialisieren wir das Array mit einer bestimmten Größe. Wir speichern die Anfangsadresse des Arrays im Adressfeld . Die set () -Methode verwendet den Index und den angegebenen Wert , die im Array gespeichert werden. Die Methode get () ruft den Bytewert mithilfe seines Index ab, der ein Offset von der Startadresse des Arrays ist.

Als nächstes können wir dieses Off-Heap-Array mithilfe seines Konstruktors zuweisen:

long SUPER_SIZE = (long) Integer.MAX_VALUE * 2; OffHeapArray array = new OffHeapArray(SUPER_SIZE);

Wir können N Zahlen von Bytewerten in dieses Array einfügen und diese Werte dann abrufen und sie summieren, um zu testen, ob unsere Adressierung korrekt funktioniert:

int sum = 0; for (int i = 0; i < 100; i++) { array.set((long) Integer.MAX_VALUE + i, (byte) 3); sum += array.get((long) Integer.MAX_VALUE + i); } assertEquals(array.size(), SUPER_SIZE); assertEquals(sum, 300);

Am Ende müssen wir den Speicher durch Aufrufen von freeMemory () wieder für das Betriebssystem freigeben.

7. CompareAndSwap Operation

Die sehr effizienten Konstrukte aus dem Paket java.concurrent wie AtomicInteger verwenden die Methoden compareAndSwap () aus Unsafe darunter, um die bestmögliche Leistung zu erzielen. Dieses Konstrukt wird häufig in sperrfreien Algorithmen verwendet, die den CAS-Prozessorbefehl nutzen können, um im Vergleich zum pessimistischen Standard-Synchronisationsmechanismus in Java eine hohe Geschwindigkeit zu erzielen.

Wir können den CAS-basierten Zähler mit der compareAndSwapLong () -Methode von Unsafe erstellen :

class CASCounter { private Unsafe unsafe; private volatile long counter = 0; private long offset; private Unsafe getUnsafe() throws IllegalAccessException, NoSuchFieldException { Field f = Unsafe.class.getDeclaredField("theUnsafe"); f.setAccessible(true); return (Unsafe) f.get(null); } public CASCounter() throws Exception { unsafe = getUnsafe(); offset = unsafe.objectFieldOffset(CASCounter.class.getDeclaredField("counter")); } public void increment() { long before = counter; while (!unsafe.compareAndSwapLong(this, offset, before, before + 1)) { before = counter; } } public long getCounter() { return counter; } }

In the CASCounter constructor we are getting the address of the counter field, to be able to use it later in the increment() method. That field needs to be declared as the volatile, to be visible to all threads that are writing and reading this value. We are using the objectFieldOffset() method to get the memory address of the offset field.

The most important part of this class is the increment() method. We're using the compareAndSwapLong() in the while loop to increment previously fetched value, checking if that previous value changed since we fetched it.

If it did, then we are retrying that operation until we succeed. There is no blocking here, which is why this is called a lock-free algorithm.

We can test our code by incrementing the shared counter from multiple threads:

int NUM_OF_THREADS = 1_000; int NUM_OF_INCREMENTS = 10_000; ExecutorService service = Executors.newFixedThreadPool(NUM_OF_THREADS); CASCounter casCounter = new CASCounter(); IntStream.rangeClosed(0, NUM_OF_THREADS - 1) .forEach(i -> service.submit(() -> IntStream .rangeClosed(0, NUM_OF_INCREMENTS - 1) .forEach(j -> casCounter.increment())));

Next, to assert that state of the counter is proper, we can get the counter value from it:

assertEquals(NUM_OF_INCREMENTS * NUM_OF_THREADS, casCounter.getCounter());

8. Park/Unpark

There are two fascinating methods in the Unsafe API that are used by the JVM to context switch threads. When the thread is waiting for some action, the JVM can make this thread blocked by using the park() method from the Unsafe class.

It is very similar to the Object.wait() method, but it is calling the native OS code, thus taking advantage of some architecture specifics to get the best performance.

Wenn der Thread blockiert ist und erneut ausgeführt werden muss, verwendet die JVM die Methode unpark () . Diese Methodenaufrufe werden häufig in Thread-Dumps angezeigt, insbesondere in Anwendungen, die Thread-Pools verwenden.

9. Fazit

In diesem Artikel haben wir uns die unsichere Klasse und ihre nützlichsten Konstrukte angesehen.

Wir haben gesehen, wie man auf private Felder zugreift, wie man Off-Heap-Speicher zuweist und wie man das Compare-and-Swap-Konstrukt verwendet, um sperrfreie Algorithmen zu implementieren.

Die Implementierung all dieser Beispiele und Codefragmente finden Sie auf GitHub - dies ist ein Maven-Projekt, daher sollte es einfach zu importieren und auszuführen sein.