Uma introdução às variáveis ​​atômicas em Java

Uma introdução às variáveis ​​atômicas em Java

1. Introdução

Simplificando, o estado compartilhado leva muito facilmente a problemas quando a concorrência está envolvida. Se o acesso a objetos mutáveis ​​compartilhados não for gerenciado corretamente, os aplicativos poderão rapidamente se tornar propensos a alguns erros de simultaneidade difíceis de detectar.

Neste artigo, vamos revisitar o uso de bloqueios para lidar com o acesso simultâneo, explorar algumas das desvantagens associadas aos bloqueios e, finalmente, apresentar variáveis ​​atômicas como uma alternativa.

2. Fechaduras

Vamos dar uma olhada na aula:

public class Counter {
    int counter;

    public void increment() {
        counter++;
    }
}

No caso de um ambiente com thread único, isso funciona perfeitamente; no entanto, assim que permitirmos que mais de um thread seja gravado, começamos a obter resultados inconsistentes.

Isso ocorre por causa da operação de incremento simples (counter++), que pode parecer uma operação atômica, mas na verdade é uma combinação de três operações: obter o valor, incrementar e escrever o valor atualizado de volta.

Se dois threads tentarem obter e atualizar o valor ao mesmo tempo, isso pode resultar em atualizações perdidas.

Uma das maneiras de gerenciar o acesso a um objeto é usar bloqueios. Isso pode ser alcançado usando a palavra-chavesynchronized na assinatura do métodoincrement. A palavra-chavesynchronized garante que apenas um encadeamento pode entrar no método por vez (para saber mais sobre bloqueio e sincronização, consulte -Guide to Synchronized Keyword in Java):

public class SafeCounterWithLock {
    private volatile int counter;

    public synchronized void increment() {
        counter++;
    }
}

Além disso, precisamos adicionar a palavra-chavevolatile para garantir a visibilidade de referência adequada entre os threads.

Usar bloqueios resolve o problema. No entanto, o desempenho sofre um golpe.

Quando vários threads tentam adquirir um bloqueio, um deles vence, enquanto o restante dos threads é bloqueado ou suspenso.

The process of suspending and then resuming a thread is very expensivee afeta a eficiência geral do sistema.

Em um programa pequeno, como ocounter, o tempo gasto na troca de contexto pode ser muito mais do que a execução real do código, reduzindo muito a eficiência geral.

3. Operações Atômicas

Há um ramo de pesquisa focado na criação de algoritmos sem bloqueio para ambientes simultâneos. Esses algoritmos exploram instruções de máquinas atômicas de baixo nível, como comparar e trocar (CAS), para garantir a integridade dos dados.

Uma operação típica do CAS funciona em três operandos:

  1. A localização da memória na qual operar (M)

  2. O valor esperado existente (A) da variável

  3. O novo valor (B) que precisa ser definido

A operação CAS atualiza atomicamente o valor em M para B, mas apenas se o valor existente em M corresponder a A, caso contrário, nenhuma ação é realizada.

Nos dois casos, o valor existente em M é retornado. Isso combina três etapas - obtendo o valor, comparando e atualizando o valor - em uma única operação no nível da máquina.

Quando vários encadeamentos tentam atualizar o mesmo valor através do CAS, um deles ganha e atualiza o valor. However, unlike in the case of locks, no other thread gets suspended; em vez disso, eles são simplesmente informados de que não conseguiram atualizar o valor. Os encadeamentos podem prosseguir com o trabalho adicional e as alternâncias de contexto são completamente evitadas.

Uma outra consequência é que a lógica do programa principal se torna mais complexa. Isso ocorre porque temos que lidar com o cenário em que a operação CAS não teve sucesso. Podemos tentar novamente até que seja bem-sucedido, ou não podemos fazer nada e seguir em frente, dependendo do caso de uso.

4. Variáveis ​​Atômicas em Java

As classes de variáveis ​​atômicas mais comumente usadas em Java sãoAtomicInteger,AtomicLong,AtomicBoolean eAtomicReference. Essas classes representamint,long,boolean e referência de objeto, respectivamente, que podem ser atualizados atomicamente. Os principais métodos expostos por essas classes são:

  • get() - obtém o valor da memória, de forma que as alterações feitas por outras threads sejam visíveis; equivalente a ler uma variávelvolatile

  • set() - grava o valor na memória, de forma que a alteração seja visível para outras threads; equivalente a escrever uma variávelvolatile

  • lazySet() - eventualmente grava o valor na memória, pode ser reordenado com subsequentes operações de memória relevantes. Um caso de uso é anular referências, para fins de coleta de lixo, que nunca serão acessadas novamente. Neste caso, um melhor desempenho é alcançado atrasando a gravaçãovolatile nula

  • compareAndSet() - igual ao descrito na seção 3, retorna verdadeiro quando for bem-sucedido, caso contrário, falso

  • weakCompareAndSet() - igual ao descrito na seção 3, mas mais fraco no sentido de que não cria ordenações acontecem antes. Isso significa que ele pode não necessariamente ver as atualizações feitas em outras variáveis

Um contador de thread safe implementado comAtomicInteger é mostrado no exemplo abaixo:

public class SafeCounterWithoutLock {
    private final AtomicInteger counter = new AtomicInteger(0);

    public int getValue() {
        return counter.get();
    }
    public void increment() {
        while(true) {
            int existingValue = getValue();
            int newValue = existingValue + 1;
            if(counter.compareAndSet(existingValue, newValue)) {
                return;
            }
        }
    }
}

Como você pode ver, tentamos novamente a operaçãocompareAndSet e novamente em caso de falha, pois queremos garantir que a chamada para o métodoincrement sempre aumenta o valor em 1.

5. Conclusão

Neste tutorial rápido, descrevemos uma maneira alternativa de lidar com a simultaneidade, na qual as desvantagens associadas ao bloqueio podem ser evitadas. Também analisamos os principais métodos expostos pelas classes de variáveis ​​atômicas em Java.

Como sempre, os exemplos estão todos disponíveisover on GitHub.

Para explorar mais classes que usam algoritmos sem bloqueio internamente, consultea guide to ConcurrentMap.