Ghid pentru sun.misc.Usafe

1. Prezentare generală

În acest articol, vom arunca o privire la o clasă fascinantă oferită de JRE - Unsafe din pachetul sun.misc . Această clasă ne oferă mecanisme de nivel scăzut care au fost concepute pentru a fi utilizate numai de biblioteca Java de bază și nu de către utilizatorii standard.

Acest lucru ne oferă mecanisme de nivel scăzut concepute în principal pentru utilizare internă în bibliotecile de bază.

2. Obținerea unei instanțe nesigure

În primul rând, pentru a putea folosi clasa Unsafe , trebuie să obținem o instanță - care nu este simplă, având în vedere că clasa a fost concepută doar pentru utilizarea internă.

Modul de a obține instanța este prin metoda statică getUnsafe (). Avertismentul este că în mod implicit - aceasta va genera o excepție SecurityException.

Din fericire, putem obține instanța folosind reflexia:

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

3. Instanțierea unei clase folosind nesigur

Să presupunem că avem o clasă simplă cu un constructor care stabilește o valoare variabilă atunci când obiectul este creat:

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

Când inițializăm acel obiect folosind constructorul, metoda getA () va returna o valoare de 1:

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

Dar putem folosi metoda allocateInstance () folosind Unsafe. Va aloca doar memoria pentru clasa noastră și nu va invoca un constructor:

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

Observați că constructorul nu a fost invocat și datorită acestui fapt, metoda getA () a returnat valoarea implicită pentru tipul lung - care este 0.

4. Modificarea câmpurilor private

Să spunem că avem o clasă care deține o valoare privată secretă :

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

Folosind metoda putInt () de la Unsafe, putem modifica o valoare a câmpului privat SECRET_VALUE , modificând / corupt starea acelei instanțe:

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

Odată ce obținem un câmp prin apelul de reflecție, îi putem modifica valoarea la orice altă valoare int folosind Unsafe .

5. Aruncarea unei excepții

Codul invocat prin Unsafe nu este examinat în același mod de către compilator ca și codul Java obișnuit. Putem folosi metoda throwException () pentru a arunca orice excepție fără a restricționa apelantul să gestioneze acea excepție, chiar dacă este o excepție verificată:

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

După aruncarea unei IOException, care este verificată, nu este nevoie să o prindem și nici să o specificăm în declarația metodei.

6. Memorie Off-Heap

Dacă o aplicație rămâne fără memorie disponibilă pe JVM, am putea ajunge să forțăm procesul GC să ruleze prea des. În mod ideal, ne-am dori o regiune de memorie specială, off-heap și care nu este controlată de procesul GC.

Metoda allocateMemory () din clasa Unsafe ne oferă posibilitatea de a aloca obiecte uriașe din grămadă, ceea ce înseamnă că această memorie nu va fi văzută și luată în considerare de GC și JVM .

Acest lucru poate fi foarte util, dar trebuie să ne amintim că această memorie trebuie gestionată manual și recuperată corect cu freeMemory () atunci când nu mai este necesară.

Să presupunem că vrem să creăm o gamă mare de octeți de memorie off-heap. Putem folosi metoda allocateMemory () pentru a realiza acest lucru:

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

În constructorul OffHeapArray, inițializăm matricea care are o dimensiune dată . Stocăm adresa de început a matricei în câmpul de adresă . Metoda set () ia indexul și valoarea dată care vor fi stocate în matrice. Metoda get () recuperează valoarea octetului utilizând indexul său, care este un offset de la adresa de pornire a matricei.

Apoi, putem aloca acea matrice off-heap folosind constructorul său:

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

Putem pune N numere de valori de octeți în această matrice și apoi preluăm acele valori, însumându-le pentru a testa dacă adresarea noastră funcționează corect:

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

În cele din urmă, trebuie să eliberăm memoria înapoi pe sistemul de operare apelând freeMemory ().

7. Comparați și schimbați operațiunea

Construcțiile foarte eficiente din pachetul java.concurrent , cum ar fi AtomicInteger, utilizează metodele compareAndSwap () din Unsafe dedesubt, pentru a oferi cele mai bune performanțe posibile. Această construcție este utilizată pe scară largă în algoritmii fără blocare care pot utiliza instrucțiunile procesorului CAS pentru a oferi o viteză mare în comparație cu mecanismul standard de sincronizare pesimistă din Java.

Putem construi contorul bazat pe CAS folosind metoda compareAndSwapLong () de la Unsafe :

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.

Când firul este blocat și trebuie să fie executabil din nou, JVM folosește metoda unpark () . Vom vedea adesea acele invocații de metode în depozite de fire, în special în aplicațiile care utilizează pool-uri de fire.

9. Concluzie

În acest articol, ne-am uitat la clasa Unsafe și la cele mai utile constructe ale acesteia.

Am văzut cum să accesăm câmpuri private, cum să alocăm memorie off-heap și cum să folosim construcția de comparare și swap pentru a implementa algoritmi fără blocare.

Implementarea tuturor acestor exemple și fragmente de cod poate fi găsită pe GitHub - acesta este un proiect Maven, deci ar trebui să fie ușor de importat și rulat așa cum este.