Leitfaden für sun.misc.Unsafe

Anleitung zusun.misc.Unsafe

1. Überblick

In diesem Artikel werfen wir einen Blick auf eine faszinierende Klasse, die vom JDK bereitgestellt wird -Unsafe aus dem Paketsun.misc. 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 in erster Linie für den internen Gebrauch in den Kernbibliotheken entwickelt wurden.

2. Erhalten einer Instanz vonUnsafe

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

The way to obtain the instance is via the static method getUnsafe(). Die Einschränkung ist, dass standardmäßig einSecurityException. ausgelöst wird

Zum Glückwe can obtain the instance using reflection:

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

3. Instanziieren einer Klasse mitUnsafe

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 MethodegetA() den Wert 1 zurück:

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

Wir können jedoch dieallocateInstance()-Methode mitUnsafe. verwenden. Sie reserviert nur den Speicher für unsere Klasse und ruft keinen Konstruktor auf:

InitializationOrdering o3
  = (InitializationOrdering) unsafe.allocateInstance(InitializationOrdering.class);

assertEquals(o3.getA(), 0);

Beachten Sie, dass der Konstruktor nicht aufgerufen wurde. Aus diesem Grund hat die MethodegetA()den Standardwert für den Typlongzurückgegeben, der 0 ist.

4. Private Felder ändern

Nehmen wir an, wir haben eine Klasse, die den privaten Wert vonsecretenthält:

class SecretHolder {
    private int SECRET_VALUE = 0;

    public boolean secretIsDisclosed() {
        return SECRET_VALUE == 1;
    }
}

Mit derputInt()-Methode vonUnsafe, können wir einen Wert des privatenSECRET_VALUE-Felds ändern, wodurch der Status dieser Instanz geändert / beschädigt wird:

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 Reflexionsaufruf ein Feld erhalten, können wir seinen Wert mitUnsafe in einen anderen Wert vonintändern.

5. Eine Ausnahme auslösen

Der Code, der überUnsafe aufgerufen wird, wird vom Compiler nicht auf die gleiche Weise geprüft wie normaler Java-Code. Wir können diethrowException()-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());
}

Nachdem wir einIOException, geworfen haben, das aktiviert ist, müssen wir es weder abfangen noch in der Methodendeklaration angeben.

6. Off-Heap-Speicher

Wenn einer Anwendung der verfügbare Speicher auf der JVM ausgeht, wird der GC-Prozess möglicherweise zu häufig ausgeführt. Im Idealfall möchten wir einen speziellen Speicherbereich, der außerhalb des Heapspeichers liegt und nicht vom GC-Prozess gesteuert wird.

DieallocateMemory()-Methode aus derUnsafe-Klasse gibt uns die Möglichkeit, große Objekte vom Heap zuzuweisen, was bedeutet, dassthis memory will not be seen and taken into account by the GC and the JVM.

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

Nehmen wir an, wir möchten das große Off-Heap-Speicherarray von Bytes erstellen. Wir können dieallocateMemory()-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 vonOffHeapArray, initialisieren wir das Array eines bestimmtensize.. Wir speichern die Anfangsadresse des Arrays im Feldaddress. Dieset()-Methode verwendet den Index und die angegebenenvalue, die im Array gespeichert werden. Dieget()-Methode ruft den Bytewert unter Verwendung ihres Index ab, der ein Versatz von der Startadresse des Arrays ist.

Als nächstes können wir dieses Off-Heap-Array mit seinem Konstruktor zuordnen:

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

Wir können N Bytewerte in dieses Array einfügen und diese Werte dann abrufen und sie aufsummieren, 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 vonfreeMemory(). an das Betriebssystem zurückgeben

7. CompareAndSwap Betrieb

Die sehr effizienten Konstrukte aus dem Paketjava.concurrent, wieAtomicInteger,, verwenden die MethodencompareAndSwap() vonUnsafe darunter, um die bestmögliche Leistung zu erzielen. Dieses Konstrukt wird häufig in den Algorithmen ohne Sperren 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 dercompareAndSwapLong()-Methode ausUnsafe konstruieren:

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;
    }
}

Im KonstruktorCASCounter erhalten wir die Adresse des Zählerfelds, um sie später in der Methodeincrement() verwenden zu können. Dieses Feld muss als flüchtig deklariert werden, um für alle Threads sichtbar zu sein, die diesen Wert schreiben und lesen. Wir verwenden dieobjectFieldOffset()-Methode, um die Speicheradresse desoffset-Felds zu erhalten.

Der wichtigste Teil dieser Klasse ist dieincrement()-Methode. Wir verwendencompareAndSwapLong() in derwhile-Schleife, um den zuvor abgerufenen Wert zu erhöhen und zu überprüfen, ob sich dieser vorherige Wert seit dem Abrufen geändert hat.

Wenn dies der Fall ist, wiederholen wir diesen Vorgang, bis wir erfolgreich sind. Hier gibt es keine Sperrung, weshalb man von einem sperrfreien Algorithmus spricht.

Wir können unseren Code testen, indem wir den gemeinsamen Zähler aus mehreren Threads erhöhen:

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())));

Als nächstes können wir den Zählerwert daraus abrufen, um sicherzustellen, dass der Zustand des Zählers korrekt ist:

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

8. Park/Unpark

In derUnsafe-API gibt es zwei faszinierende Methoden, die von der JVM zum Kontextwechsel von Threads verwendet werden. Wenn der Thread auf eine Aktion wartet, kann die JVM diesen Thread mithilfe der Methodepark() aus der KlasseUnsafeblockieren.

Es ist der MethodeObject.wait()ehr ähnlich, ruft jedoch den nativen Betriebssystemcode auf und nutzt daher einige Architekturspezifikationen, um die beste Leistung zu erzielen.

When the thread is blocked and needs to be made runnable again, the JVM uses the unpark() method. 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 KlasseUnsafeund ihr nützlichstes Konstrukt angesehen.

Wir haben gesehen, wie auf private Felder zugegriffen wird, wie Off-Heap-Speicher zugewiesen wird und wie das Compare-and-Swap-Konstrukt verwendet wird, um Algorithmen ohne Sperren zu implementieren.

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