Gids voor sun.misc.Unsafe

1. Overzicht

In dit artikel zullen we een fascinerende les van de JRE bekijken - Onveilig van de zon.misc pakket. Deze klasse biedt ons mechanismen op laag niveau die zijn ontworpen om alleen te worden gebruikt door de Java-kernbibliotheek en niet door standaardgebruikers.

Dit biedt ons low-level mechanismen die primair zijn ontworpen voor intern gebruik binnen de kernbibliotheken.

2. Het verkrijgen van een exemplaar van het Onveilig

Ten eerste om de Onveilig class, moeten we een instantie krijgen - wat niet eenvoudig is, aangezien de klasse alleen is ontworpen voor intern gebruik.

De manier om de instantie te verkrijgen is via de statische methode getUnsafe (). Het voorbehoud is dat dit standaard een Beveiligingsuitzondering.

Gelukkig, we kunnen de instantie verkrijgen met behulp van reflectie:

Veld f = Unsafe.class.getDeclaredField ("theUnsafe"); f.setAccessible (true); onveilig = (Onveilig) f.get (null);

3. Instantiëren van een klas met behulp van Onveilig

Laten we zeggen dat we een eenvoudige klasse hebben met een constructor die een variabele waarde instelt wanneer het object wordt gemaakt:

class InitializationOrdering {privé lang a; openbare InitializationOrdering () {this.a = 1; } public long getA () {retourneer this.a; }}

Wanneer we dat object initialiseren met behulp van de constructor, wordt de krijg een() methode retourneert een waarde van 1:

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

Maar we kunnen de allocateInstance () methode gebruiken Onveilig. Het wijst alleen het geheugen toe aan onze klasse en roept geen constructor op:

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

Merk op dat de constructor niet is aangeroepen en dat daardoor het krijg een() methode retourneerde de standaardwaarde voor de lang type - dat is 0.

4. Wijzigen van privévelden

Laten we zeggen dat we een klas hebben met een geheim privéwaarde:

klasse SecretHolder {privé int SECRET_VALUE = 0; openbare boolean secretIsDisclosed () {retourneer SECRET_VALUE == 1; }}

De ... gebruiken putInt () methode van Onveilig, we kunnen een waarde van het privé veranderen SECRET_VALUE veld, de status van die instantie wijzigen / beschadigen:

SecretHolder secretHolder = nieuwe SecretHolder (); Veld f = secretHolder.getClass (). GetDeclaredField ("SECRET_VALUE"); unsafe.putInt (secretHolder, unsafe.objectFieldOffset (f), 1); assertTrue (secretHolder.secretIsDisclosed ());

Zodra we een veld krijgen door de reflectieoproep, kunnen we de waarde ervan wijzigen in een ander int waarde met behulp van de Onveilig.

5. Een uitzondering werpen

De code die wordt aangeroepen via Onveilig wordt door de compiler niet op dezelfde manier onderzocht als gewone Java-code. We kunnen de throwException () methode om een ​​uitzondering te genereren zonder de beller te beperken om die uitzondering af te handelen, zelfs als het een aangevinkte uitzondering is:

@Test (verwacht = IOException.class) openbare ongeldige gegevenUnsafeThrowException_whenThrowCheckedException_thenNotNeedToCatchIt () {unsafe.throwException (nieuwe IOException ()); }

Na het gooien van een IOException, die is aangevinkt, hoeven we het niet te vangen of te specificeren in de methode-declaratie.

6. Off-Heap-geheugen

Als een applicatie onvoldoende geheugen op de JVM heeft, kunnen we het GC-proces te vaak dwingen. Idealiter zouden we een speciaal geheugengebied willen, off-heap en niet gecontroleerd door het GC-proces.

De toewijzenMemory () methode van de Onveilig class geeft ons de mogelijkheid om enorme objecten van de hoop toe te wijzen, wat betekent dat deze herinnering zal niet worden gezien en in aanmerking worden genomen door de GC en de JVM.

Dit kan erg handig zijn, maar we moeten niet vergeten dat dit geheugen handmatig moet worden beheerd en correct moet worden teruggewonnen met vrij geheugen() wanneer niet langer nodig.

Laten we zeggen dat we de grote niet-heap-geheugenarray van bytes willen maken. We kunnen de toewijzenMemory () methode om dat te bereiken:

class OffHeapArray {privé definitieve statische int BYTE = 1; privé lang formaat; privé lang adres; openbare OffHeapArray (lange grootte) gooit NoSuchFieldException, IllegalAccessException {this.size = size; adres = getUnsafe (). allocateMemory (grootte * BYTE); } private Unsafe getUnsafe () gooit IllegalAccessException, NoSuchFieldException {Field f = Unsafe.class.getDeclaredField ("theUnsafe"); f.setAccessible (true); return (onveilig) f.get (null); } public void set (long i, byte value) gooit NoSuchFieldException, IllegalAccessException {getUnsafe (). putByte (adres + i * BYTE, waarde); } public int get (lange idx) gooit NoSuchFieldException, IllegalAccessException {return getUnsafe (). getByte (adres + idx * BYTE); } public long size () {return size; } public void freeMemory () gooit NoSuchFieldException, IllegalAccessException {getUnsafe (). freeMemory (adres); }
}

In de constructor van de OffHeapArray, we initialiseren de array die van een gegeven is grootte. We slaan het beginadres van de array op in de adres veld. De set () methode is het nemen van de index en het gegeven waarde dat wordt opgeslagen in de array. De krijgen() methode is het ophalen van de bytewaarde met behulp van de index die een offset is ten opzichte van het startadres van de array.

Vervolgens kunnen we die off-heap-array toewijzen met behulp van de constructor:

lang SUPER_SIZE = (lang) Geheel getal.MAX_VALUE * 2; OffHeapArray-array = nieuwe OffHeapArray (SUPER_SIZE);

We kunnen N aantallen bytewaarden in deze array plaatsen en vervolgens die waarden ophalen, ze optellen om te testen of onze adressering correct werkt:

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

Uiteindelijk moeten we het geheugen weer vrijgeven aan het besturingssysteem door te bellen vrij geheugen().

7. CompareAndSwap Operatie

De zeer efficiënte constructies van de java.concurrent pakket, zoals Atomic Geheel getal, gebruiken de vergelijkAndSwap () methoden uit Onveilig onderaan, om de best mogelijke prestaties te bieden. Dit construct wordt veel gebruikt in de vergrendelingsvrije algoritmen die de CAS-processorinstructie kunnen gebruiken om een ​​grote versnelling te bieden in vergelijking met het standaard pessimistische synchronisatiemechanisme in Java.

We kunnen de CAS-gebaseerde teller construeren met behulp van de vergelijkAndSwapLong () methode van Onveilig:

klasse CASCounter {privé Onveilig onveilig; privé vluchtige lange teller = 0; privé lange compensatie; private Unsafe getUnsafe () gooit IllegalAccessException, NoSuchFieldException {Field f = Unsafe.class.getDeclaredField ("theUnsafe"); f.setAccessible (true); return (onveilig) f.get (null); } public CASCounter () gooit uitzondering {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 () {retourteller; }}

In de CASCounter constructor krijgen we het adres van het tellerveld, om het later in het toename () methode. Dat veld moet als vluchtig worden gedeclareerd om zichtbaar te zijn voor alle threads die deze waarde schrijven en lezen. We gebruiken de objectFieldOffset () methode om het geheugenadres van het compensatie veld.

Het belangrijkste onderdeel van deze les is de toename () methode. We gebruiken de vergelijkAndSwapLong () in de terwijl loop om de eerder opgehaalde waarde te verhogen, waarbij wordt gecontroleerd of die vorige waarde is gewijzigd sinds we deze hebben opgehaald.

Als dit het geval is, proberen we die bewerking opnieuw totdat het lukt. Er is hier geen blokkering, daarom wordt dit een vergrendelingsvrij algoritme genoemd.

We kunnen onze code testen door de gedeelde teller van meerdere threads te verhogen:

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

Om vervolgens te beweren dat de staat van de teller juist is, kunnen we de tellerwaarde eruit halen:

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

8. Parkeren / uitparkeren

Er zijn twee fascinerende methoden in de Onveilig API die door de JVM wordt gebruikt om van context te wisselen tussen threads. Wanneer de thread op een actie wacht, kan de JVM deze thread blokkeren door de park() methode van de Onveilig klasse.

Het lijkt erg op de Object.wait () methode, maar het roept de native OS-code aan, waardoor wordt geprofiteerd van enkele architectuurspecificaties om de beste prestaties te krijgen.

Wanneer de thread is geblokkeerd en opnieuw uitvoerbaar moet worden gemaakt, gebruikt de JVM de uitparkeren () methode. We zullen die methode-aanroepen vaak zien in thread-dumps, vooral in de applicaties die thread-pools gebruiken.

9. Conclusie

In dit artikel keken we naar de Onveilig class en de meest bruikbare constructies.

We hebben gezien hoe je toegang krijgt tot privévelden, hoe je off-heap-geheugen toewijst en hoe je de vergelijk-en-wisselconstructie gebruikt om vergrendelingsvrije algoritmen te implementeren.

De implementatie van al deze voorbeelden en codefragmenten is te vinden op GitHub - dit is een Maven-project, dus het moet gemakkelijk te importeren en uit te voeren zijn zoals het is.