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á.