LongAdder e LongAccumulator em Java

LongAdder eLongAccumulator em Java

1. Visão geral

Neste artigo, veremos duas construções do pacotejava.util.concurrent:LongAddereLongAccumulator.

Ambos são criados para serem muito eficientes no ambiente multi-threaded e ambos utilizam táticas muito inteligentes para seremlock-free and still remain thread-safe.

2. LongAdder

Vamos considerar alguma lógica que está incrementando alguns valores com muita frequência, onde usar umAtomicLong pode ser um gargalo. Isso usa uma operação de comparação e troca, que - sob forte contenção - pode levar a muitos ciclos de CPU desperdiçados.

LongAdder, por outro lado, usa um truque muito inteligente para reduzir a contenção entre threads, quando estes a estão incrementando.

Quando queremos incrementar uma instância deLongAdder,, precisamos chamar o métodoincrement(). Essa implementaçãokeeps an array of counters that can grow on demand.

E assim, quando mais threads estiverem chamandoincrement(), o array será mais longo. Cada registro na matriz pode ser atualizado separadamente - reduzindo a contenção. Devido a esse fato, oLongAdder é uma maneira muito eficiente de incrementar um contador de vários threads.

Vamos criar uma instância da classeLongAdder e atualizá-la a partir de vários 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);
}

O resultado do contador emLongAdder não está disponível até que chamemos o métodosum(). Esse método irá percorrer todos os valores da matriz abaixo e somará esses valores retornando o valor apropriado. Precisamos ter cuidado, pois a chamada para o métodosum() pode ser muito cara:

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

Às vezes, depois de chamarsum(), queremos limpar todos os estados associados à instância deLongAddere começar a contar do início. Podemos usar o métodosumThenReset() para conseguir isso:

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

Observe que a chamada subsequente ao métodosum() retorna zero, o que significa que o estado foi redefinido com sucesso.

3. LongAccumulator

LongAccumulator também é uma classe muito interessante - o que nos permite implementar um algoritmo livre de bloqueio em vários cenários. Por exemplo, ele pode ser usado para acumular resultados de acordo com oLongBinaryOperator fornecido - isso funciona de forma semelhante à operaçãoreduce() da API Stream.

A instância deLongAccumulator pode ser criada fornecendoLongBinaryOperatore o valor inicial para seu construtor. É importante lembrar 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);

Estamos criando umLongAccumulator which adicionará um novo valor ao valor que já estava no acumulador. Estamos definindo o valor inicial deLongAccumulator como zero, portanto, na primeira chamada do métodoaccumulate(),previousValue terá um valor zero.

Vamos invocar o métodoaccumulate() de vários 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);
}

Observe como estamos passando um número como um argumento para o métodoaccumulate(). Esse método invocará nossa funçãosum().

OLongAccumulator está usando a implementação compare-and-swap - o que leva a essas semânticas interessantes.

Primeiramente, ele executa uma ação definida comoLongBinaryOperator,e então verifica se opreviousValue mudou. Se foi alterado, a ação é executada novamente com o novo valor. Caso contrário, é possível alterar o valor armazenado no acumulador.

Agora podemos afirmar que a soma de todos os valores de todas as iterações foi20200:

assertEquals(accumulator.get(), 20200);

4. Conclusão

Neste tutorial rápido, demos uma olhada emLongAddereLongAccumulatore mostramos como usar ambas as construções para implementar soluções muito eficientes e sem bloqueio.

A implementação de todos esses exemplos e trechos de código pode ser encontrada emGitHub project - este é um projeto Maven, portanto, deve ser fácil de importar e executar como está.