LongAdder et LongAccumulator en Java

LongAdder etLongAccumulator en Java

1. Vue d'ensemble

Dans cet article, nous allons examiner deux constructions du packagejava.util.concurrent:LongAdder etLongAccumulator.

Les deux sont créés pour être très efficaces dans l'environnement multi-thread et tous deux tirent parti de tactiques très intelligentes pour êtrelock-free and still remain thread-safe.

2. LongAdder

Examinons une logique qui incrémente très souvent certaines valeurs, où l'utilisation d'unAtomicLong peut être un goulot d'étranglement. Cette opération utilise une opération de comparaison et d'échange qui, en cas de vive controverse, peut entraîner de nombreux cycles de processeur gaspillés.

LongAdder, d'autre part, utilise une astuce très intelligente pour réduire les conflits entre les threads, lorsque ceux-ci l'incrémentent.

Lorsque nous voulons incrémenter une instance desLongAdder,, nous devons appeler la méthodeincrement(). Cette implémentationkeeps an array of counters that can grow on demand.

Et ainsi, quand plus de threads appellentincrement(), le tableau sera plus long. Chaque enregistrement du tableau peut être mis à jour séparément, ce qui réduit les conflits. De ce fait, leLongAdder est un moyen très efficace pour incrémenter un compteur à partir de plusieurs threads.

Créons une instance de la classeLongAdder et mettons-la à jour à partir de plusieurs threads:

LongAdder counter = new LongAdder();
ExecutorService executorService = Executors.newFixedThreadPool(8);

int numberOfThreads = 4;
int numberOfIncrements = 100;

Runnable incrementAction = () -> IntStream
  .range(0, numberOfIncrements)
  .forEach(i -> counter.increment());

for (int i = 0; i < numberOfThreads; i++) {
    executorService.execute(incrementAction);
}

Le résultat du compteur dans lesLongAdder n'est pas disponible jusqu'à ce que nous appelions la méthodesum(). Cette méthode itérera sur toutes les valeurs du tableau inférieur et additionnera ces valeurs en renvoyant la valeur appropriée. Nous devons cependant faire attention car l'appel à la méthodesum() peut être très coûteux:

assertEquals(counter.sum(), numberOfIncrements * numberOfThreads);

Parfois, après avoir appelésum(), nous voulons effacer tous les états associés à l'instance deLongAdder et commencer à compter depuis le début. Nous pouvons utiliser la méthodesumThenReset() pour y parvenir:

assertEquals(counter.sumThenReset(), numberOfIncrements * numberOfThreads);
assertEquals(counter.sum(), 0);

Notez que l'appel suivant à la méthodesum() renvoie zéro, ce qui signifie que l'état a été réinitialisé avec succès.

3. LongAccumulateur

LongAccumulator est également une classe très intéressante - qui nous permet d'implémenter un algorithme sans verrouillage dans un certain nombre de scénarios. Par exemple, il peut être utilisé pour accumuler des résultats en fonction desLongBinaryOperator fournis - cela fonctionne de manière similaire à l'opérationreduce() de Stream API.

L'instance desLongAccumulator peut être créée en fournissant lesLongBinaryOperator et la valeur initiale à son constructeur. La chose importante à retenir queLongAccumulator will work correctly if we supply it with a commutative function where the order of accumulation does not matter.

LongAccumulator accumulator = new LongAccumulator(Long::sum, 0L);

Nous créons unLongAccumulator which ajoutera une nouvelle valeur à la valeur qui était déjà dans l'accumulateur. Nous définissons la valeur initiale desLongAccumulator à zéro, donc lors du premier appel de la méthodeaccumulate(), lespreviousValue auront une valeur nulle.

Appelons la méthodeaccumulate() à partir de plusieurs threads:

int numberOfThreads = 4;
int numberOfIncrements = 100;

Runnable accumulateAction = () -> IntStream
  .rangeClosed(0, numberOfIncrements)
  .forEach(accumulator::accumulate);

for (int i = 0; i < numberOfThreads; i++) {
    executorService.execute(accumulateAction);
}

Remarquez comment nous transmettons un nombre comme argument à la méthodeaccumulate(). Cette méthode invoquera notre fonctionsum().

LeLongAccumulator utilise l'implémentation compare-and-swap - ce qui conduit à cette sémantique intéressante.

Tout d'abord, il exécute une action définie comme unLongBinaryOperator, puis il vérifie si lepreviousValue a changé. Si elle a été modifiée, l'action est exécutée à nouveau avec la nouvelle valeur. Sinon, il réussit à modifier la valeur stockée dans l'accumulateur.

Nous pouvons maintenant affirmer que la somme de toutes les valeurs de toutes les itérations était de20200:

assertEquals(accumulator.get(), 20200);

4. Conclusion

Dans ce rapide tutoriel, nous avons examinéLongAdder etLongAccumulator et nous avons montré comment utiliser les deux constructions pour implémenter des solutions très efficaces et sans verrouillage.

L'implémentation de tous ces exemples et extraits de code peut être trouvée dans leGitHub project - il s'agit d'un projet Maven, il devrait donc être facile à importer et à exécuter tel quel.