Guide pour sun.misc.Unsafe

Guide desun.misc.Unsafe

1. Vue d'ensemble

Dans cet article, nous examinerons une classe fascinante fournie par le JDK -Unsafe du packagesun.misc. Cette classe nous fournit des mécanismes de bas niveau conçus pour être utilisés uniquement par la bibliothèque Java principale et non par les utilisateurs standard.

Cela nous fournit des mécanismes de bas niveau principalement conçus pour une utilisation interne dans les bibliothèques principales.

2. Obtention d'une instance desUnsafe

Premièrement, pour pouvoir utiliser la classeUnsafe, nous devons obtenir une instance - ce qui n'est pas simple étant donné que la classe a été conçue uniquement pour l'usage interne.

The way to obtain the instance is via the static method getUnsafe(). La mise en garde est que par défaut - cela lancera unSecurityException.

Heureusement,we can obtain the instance using reflection:

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

3. Instanciation d'une classe à l'aide deUnsafe

Disons que nous avons une classe simple avec un constructeur qui définit une valeur de variable lors de la création de l'objet:

class InitializationOrdering {
    private long a;

    public InitializationOrdering() {
        this.a = 1;
    }

    public long getA() {
        return this.a;
    }
}

Lorsque nous initialisons cet objet en utilisant le constructeur, la méthodegetA() retournera une valeur de 1:

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

Mais nous pouvons utiliser la méthodeallocateInstance() en utilisantUnsafe. Cela n'allouera que la mémoire pour notre classe, et n'appellera pas de constructeur:

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

assertEquals(o3.getA(), 0);

Notez que le constructeur n'a pas été appelé et de ce fait, la méthodegetA() a renvoyé la valeur par défaut pour le typelong - qui est 0.

4. Modification des champs privés

Disons que nous avons une classe qui contient une valeur privéesecret:

class SecretHolder {
    private int SECRET_VALUE = 0;

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

En utilisant la méthodeputInt() deUnsafe,, nous pouvons changer une valeur du champ privéSECRET_VALUE, en changeant / corrompant l'état de cette instance:

SecretHolder secretHolder = new SecretHolder();

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

assertTrue(secretHolder.secretIsDisclosed());

Une fois que nous obtenons un champ par l'appel de réflexion, nous pouvons modifier sa valeur en n'importe quelle autre valeurint en utilisant leUnsafe.

5. Lancer une exception

Le code qui est appelé viaUnsafe n'est pas examiné de la même manière par le compilateur que le code Java normal. Nous pouvons utiliser la méthodethrowException() pour lever une exception sans restreindre l'appelant à gérer cette exception, même s'il s'agit d'une exception vérifiée:

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

Après avoir lancé unIOException, qui est vérifié, nous n'avons pas besoin de l'attraper ni de le spécifier dans la déclaration de méthode.

6. Mémoire hors tas

Si une application manque de mémoire disponible sur la machine virtuelle, nous pourrions éventuellement forcer le processus de GC à s'exécuter trop souvent. Idéalement, nous souhaiterions une région de mémoire spéciale, non gérée et non contrôlée par le processus GC.

La méthodeallocateMemory() de la classeUnsafe nous donne la possibilité d'allouer des objets énormes hors du tas, ce qui signifie quethis memory will not be seen and taken into account by the GC and the JVM.

Cela peut être très utile, mais nous devons nous rappeler que cette mémoire doit être gérée manuellement et correctement récupérée avecfreeMemory() lorsqu'elle n'est plus nécessaire.

Disons que nous voulons créer le grand tableau de mémoire hors tas d'octets. Nous pouvons utiliser la méthodeallocateMemory() pour y parvenir:

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

Dans le constructeur duOffHeapArray,, nous initialisons le tableau correspondant à unsize. donné. Nous stockons l’adresse de début du tableau dans le champaddress. La méthodeset() prend l'index et lesvalue donnés qui seront stockés dans le tableau. La méthodeget() récupère la valeur d'octet à l'aide de son index qui est un décalage par rapport à l'adresse de début du tableau.

Ensuite, nous pouvons allouer ce tableau hors tas en utilisant son constructeur:

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

Nous pouvons placer N nombres d'octets dans ce tableau, puis récupérer ces valeurs et les additionner pour vérifier si notre adressage fonctionne correctement:

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

En fin de compte, nous devons libérer la mémoire vers le système d'exploitation en appelantfreeMemory().

7. OpérationCompareAndSwap

Les constructions très efficaces du packagejava.concurrent, commeAtomicInteger,, utilisent les méthodescompareAndSwap() surUnsafe ci-dessous, pour fournir les meilleures performances possibles. Cette construction est largement utilisée dans les algorithmes sans verrouillage qui peuvent exploiter l'instruction de processeur CAS pour fournir une grande rapidité par rapport au mécanisme de synchronisation pessimiste standard en Java.

Nous pouvons construire le compteur basé sur CAS en utilisant la méthodecompareAndSwapLong() à partir deUnsafe:

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

Dans le constructeurCASCounter, nous obtenons l'adresse du champ compteur, pour pouvoir l'utiliser plus tard dans la méthodeincrement(). Ce champ doit être déclaré volatile et visible par tous les threads qui écrivent et lisent cette valeur. Nous utilisons la méthodeobjectFieldOffset() pour obtenir l'adresse mémoire du champoffset.

La partie la plus importante de cette classe est la méthodeincrement(). Nous utilisons lescompareAndSwapLong() dans la bouclewhile pour incrémenter la valeur précédemment extraite, en vérifiant si cette valeur précédente a changé depuis que nous l'avons récupérée.

Si c'est le cas, nous réessayons l'opération jusqu'à ce que nous réussissions. Il n'y a pas de blocage ici, c'est pourquoi on appelle cela un algorithme sans verrouillage.

Nous pouvons tester notre code en incrémentant le compteur partagé à partir de plusieurs 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())));

Ensuite, pour affirmer que l'état du compteur est correct, nous pouvons en obtenir la valeur de compteur:

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

8. Park/Unpark

Il existe deux méthodes fascinantes dans l'APIUnsafe qui sont utilisées par la JVM pour changer de thread de contexte. Lorsque le thread attend une action, la JVM peut bloquer ce thread en utilisant la méthodepark() de la classeUnsafe.

Elle est très similaire à la méthodeObject.wait(), mais elle appelle le code natif du système d'exploitation, profitant ainsi de certaines spécificités de l'architecture pour obtenir les meilleures performances.

When the thread is blocked and needs to be made runnable again, the JVM uses the unpark() method. Nous verrons souvent ces invocations de méthodes dans les thread dumps, en particulier dans les applications qui utilisent des pools de threads.

9. Conclusion

Dans cet article, nous examinions la classeUnsafe et sa construction la plus utile.

Nous avons vu comment accéder à des champs privés, comment allouer de la mémoire extra-plate et comment utiliser la structure de comparaison et d'échange pour implémenter des algorithmes sans verrouillage.

L'implémentation de tous ces exemples et extraits de code peut être trouvéeover on GitHub - c'est un projet Maven, il devrait donc être facile à importer et à exécuter tel quel.