Java 9 poignées variables démystifiées

Java 9 poignées variables démystifiées

1. introduction

Java 9 a apporté un certain nombre de nouvelles fonctionnalités utiles pour les développeurs.

L’un d’eux est l’APIjava.lang.invoke.VarHandle - représentant des descripteurs de variables - que nous allons explorer dans cet article.

2. Que sont les poignées variables?

Généralement,a variable handle is just a typed reference to a variable. La variable peut être un élément de tableau, une instance ou un champ statique de la classe.

La classeVarHandle fournit un accès en écriture et en lecture aux variables dans des conditions spécifiques.

VarHandles sont immuables et n'ont aucun état visible. De plus, ils ne peuvent pas être sous-classés.

ChaqueVarHandle a:

  • un type génériqueT, qui est le type de chaque variable représentée par ceVarHandle

  • une liste de types de coordonnéesCT, qui sont des types d'expressions de coordonnées, qui permettent de localiser la variable référencée par ceVarHandle

La liste des types de coordonnées peut être vide.

Le but deVarHandle est de définir une norme pour appeler les opérationsequivalents ofjava.util.concurrent.atomic etsun.misc.Unsafe sur les champs et les éléments de tableau.

Ces opérations sont en majorité des opérations atomiques ou ordonnées. Par exemple, incrémentation de champ atomique.

3. Création de poignées variables

Pour utiliser lesVarHandle, nous devons d'abord avoir des variables.

Déclarons une classe simple avec différentes variables de typeint que nous utiliserons dans nos exemples:

public class VariableHandlesTest {
    public int publicTestVariable = 1;
    private int privateTestVariable = 1;
    public int variableToSet = 1;
    public int variableToCompareAndSet = 1;
    public int variableToGetAndAdd = 0;
    public byte variableToBitwiseOr = 0;
}

3.1. Poignées de variables pour les variables publiques

Now we can get a VarHandle for our publicTestVariable using the findVarHandle() method:

VarHandle publicIntHandle = MethodHandles.lookup()
  .in(VariableHandlesTest.class)
  .findVarHandle(VariableHandlesTest.class, "publicTestVariable", int.class);

assertThat(
  publicIntHandle.coordinateTypes().size() == 1);
assertThat(
  publicIntHandle.coordinateTypes().get(0) == VariableHandles.class);

Nous pouvons voir quecoordinateTypes de ceVarHandle n'est pas vide et a un élément, qui est notre classeVariableHandlesTest.

3.2. Poignées de variables pour les variables privées

Si nous avons un membre privé et que nous avons besoin d'un handle de variable pour une telle variablewe can obtain this using the privateLookupIn() method:

VarHandle privateIntHandle = MethodHandles
  .privateLookupIn(VariableHandlesTest.class, MethodHandles.lookup())
  .findVarHandle(VariableHandlesTest.class, "privateTestVariable", int.class);

assertThat(privateIntHandle.coordinateTypes().size() == 1);
assertThat(privateIntHandle.coordinateTypes().get(0) == VariableHandlesTest.class);

Ici, nous avons choisi la méthodeprivateLookupIn() qui a un accès plus large que la normalelookup(). Cela nous permet d'accéder aux variablesprivate, public ouprotected.

Avant Java 9, l'API équivalente pour cette opération était la méthodeUnsafe class_ and the _setAccessible() de l'APIReflection.

Cependant, cette approche a ses inconvénients. Par exemple, cela ne fonctionnera que pour l'instance spécifique de la variable.

VarHandle est une solution meilleure et plus rapide dans de tels cas.

3.3. Poignées variables pour les tableaux

Nous pourrions utiliser la syntaxe précédente pour obtenir des champs de tableau.

Cependant, nous pouvons également obtenir lesVarHandle pour un tableau de type spécifique:

VarHandle arrayVarHandle = MethodHandles.arrayElementVarHandle(int[].class);

assertThat(arrayVarHandle.coordinateTypes().size() == 2);
assertThat(arrayVarHandle.coordinateTypes().get(0) == int[].class);

Nous pouvons maintenant voir que cesVarHandle ont deux types de coordonnéesint et[], qui représentent un tableau de primitivesint.

4. Appel des méthodesVarHandle

Most of the VarHandle methods expect a variable number of arguments of typeObject. L'utilisation deObject… comme argument désactive la vérification des arguments statiques.

Tous les arguments sont vérifiés au moment de l'exécution. En outre, différentes méthodes s'attendent à avoir un nombre différent d'arguments de types différents.

Si nous ne parvenons pas à donner un nombre correct d'arguments avec des types appropriés, l'appel de méthode lancera unWrongMethodTypeException.

Par exemple,get() attendra au moins un argument, ce qui aidera à localiser la variable, maisset() attendra un autre argument qui est la valeur à affecter à la variable.

5. Modes d'accès aux poignées variables

En général, toutes les méthodes de la classeVarHandle __ tombent dans cinq modes d'accès différents.

Passons en revue chacun d’entre eux dans les sous-sections suivantes.

5.1. Accès en lecture

Les méthodes avec un niveau d'accès en lecture permettent d'obtenir la valeur de la variable sous des effets de classement en mémoire spécifiés. Il existe plusieurs méthodes avec ce mode d'accès comme:get(), getAcquire(), getVolatile() etgetOpaque().

Nous pouvons facilement utiliser la méthodeget() sur nosVarHandle:

assertThat((int) publicIntHandle.get(this) == 1);

La méthodeget() ne prend queCoordinateTypes comme paramètres, nous pouvons donc simplement utiliserthis dans notre cas.

5.2. Accès en écriture

Les méthodes avec un niveau d'accès en écriture nous permettent de définir la valeur de la variable sous des effets d'ordonnancement de la mémoire spécifiques.

Comme pour les méthodes avec accès en lecture, nous avons plusieurs méthodes avec accès en écriture:set(), setOpaque(), setVolatile() etsetRelease().

Nous pouvons utiliser la méthodeset() sur nosVarHandle:

publicIntHandle.set(this, 15);

assertThat((int) publicIntHandle.get(this) == 15);

La méthodeset() attend au moins deux arguments. Le premier aidera à localiser la variable, tandis que le second est la valeur à définir pour la variable.

5.3. Accès aux mises à jour atomiques

Les méthodes avec ce niveau d'accès peuvent être utilisées pour mettre à jour de manière atomique la valeur de la variable.

Utilisons la méthodecompareAndSet() pour voir les effets:

publicIntHandle.compareAndSet(this, 1, 100);

assertThat((int) publicIntHandle.get(this) == 100);

En dehors de la méthodeCoordinateTypes, t, ellecompareAndSet() prend deux valeurs supplémentaires:oldValue etnewValue. La méthode définit la valeur de la variable si elle était égale àoldVariable ou quitte il n'a pas changé autrement.

5.4. Accès aux mises à jour atomiques numériques

Ces méthodes permettent d'effectuer des opérations numériques telles quegetAndAdd () sous des effets d'ordre mémoire spécifiques.

Voyons comment nous pouvons effectuer des opérations atomiques en utilisant unVarHandle:

int before = (int) publicIntHandle.getAndAdd(this, 200);

assertThat(before == 0);
assertThat((int) publicIntHandle.get(this) == 200);

Ici, la méthodegetAndAdd() retourne d'abord la valeur de la variable, puis ajoute la valeur fournie.

5.5. Accès aux mises à jour atomiques au niveau du bit

Les méthodes avec cet accès permettent d'effectuer de manière atomique des opérations au niveau du bit sous des effets de classement en mémoire spécifiques.

Voyons un exemple d’utilisation de la méthodegetAndBitwiseOr():

byte before = (byte) publicIntHandle.getAndBitwiseOr(this, (byte) 127);

assertThat(before == 0);
assertThat(variableToBitwiseOr == 127);

Cette méthode obtiendra la valeur de notre variable et effectuera une opération OU au niveau du bit.

L'appel de méthode lèvera unIllegalAccessException s'il ne parvient pas à faire correspondre le mode d'accès requis par la méthode avec celui autorisé par la variable.

Par exemple, cela se produira si nous essayons d'utiliser une méthodeset() sur une variablefinal.

6. Effets d'ordre de la mémoire

Nous avons mentionné précédemment queVarHandle methods allow access to variables under specific memory ordering effects.

Pour la plupart des méthodes, il existe 4 effets de classement en mémoire:

  • Les lectures et écritures dePlain garantissent l'atomicité bit à bit pour les références et primitives de moins de 32 bits. De plus, ils n’imposent aucune contrainte d’ordre aux autres traits.

  • Les opérationsOpaque sont atomiques au niveau du bit et ordonnées de manière cohérente par rapport à l'accès à la même variable.

  • Les opérationsAcquire etRelease obéissent aux propriétésOpaque. De plus, les lectures deAcquire seront ordonnées uniquement après les écritures en modeRelease correspondantes.

  • Les opérationsVolatile sont entièrement ordonnées les unes par rapport aux autres.

Il est très important de se rappeler queaccess modes will override previous memory ordering effects. Cela signifie que, par exemple, si nous utilisonsget(), ce sera une opération de lecture simple, même si nous avons déclaré notre variable commevolatile.

Pour cette raison, les développeurs doivent faire preuve d'une extrême prudence lorsqu'ils utilisent les opérationsVarHandle.

7. Conclusion

Dans ce tutoriel, nous avons présenté les descripteurs de variable et leur utilisation.

Ce sujet est assez compliqué car les descripteurs de variables visent à permettre des manipulations de bas niveau et ne doivent pas être utilisés sauf si nécessaire.

Comme toujours, les exemples de code sont disponiblesover on GitHub.