Quando usar o AtomicReference em Java?

311

Quando usamos AtomicReference?

É necessário criar objetos em todos os programas multithread?

Forneça um exemplo simples em que AtomicReference deve ser usado.

Chintu
fonte

Respostas:

215

A referência atômica deve ser usada em uma configuração na qual você precisa executar operações atômicas simples (por exemplo, com segurança de thread , não triviais) em uma referência, para a qual a sincronização baseada em monitor não é apropriada. Suponha que você queira verificar se um campo específico apenas se o estado do objeto permanece como você verificou pela última vez:

AtomicReference<Object> cache = new AtomicReference<Object>();

Object cachedValue = new Object();
cache.set(cachedValue);

//... time passes ...
Object cachedValueToUpdate = cache.get();
//... do some work to transform cachedValueToUpdate into a new version
Object newValue = someFunctionOfOld(cachedValueToUpdate);
boolean success = cache.compareAndSet(cachedValue,cachedValueToUpdate);

Por causa da semântica de referência atômica, você pode fazer isso mesmo se o cacheobjeto for compartilhado entre encadeamentos, sem usar synchronized. Em geral, é melhor usar sincronizadores ou a java.util.concurrentestrutura, em vez de usar a Atomic*menos que saiba o que está fazendo.

Duas excelentes referências de árvores mortas que apresentarão esse tópico:

Observe que (não sei se isso sempre foi verdade) a atribuição de referência (por exemplo =) é atômica (atualizando tipos primitivos de 64 bits como longou doublepode não ser atômica; mas a atualização de uma referência é sempre atômica, mesmo que seja de 64 bits ) sem usar explicitamente um Atomic*.
Consulte a Especificação de linguagem Java 3ed, Seção 17.7 .

andersoj
fonte
43
Corrija-me se estiver errado, mas parece que a chave para isso é porque você precisa fazer um "compareAndSet". Se tudo o que eu precisava fazer fosse definido, eu não precisaria do AtomicObject, porque as atualizações de referência são atômicas?
precisa saber é o seguinte
É seguro fazer cache.compareAndSet (cachedValue, someFunctionOfOld (cachedValueToUpdate))? Ou seja, inline o cálculo?
kaqqao
4
@veggen Os argumentos da função em Java são avaliados antes da própria função; portanto, inlining não faz diferença neste caso. Sim, é seguro.
Dmitry
29
@sMoZely Isso está correto, mas se você não estiver usando AtomicReference, marque a variável volatileporque, embora o tempo de execução garanta que a atribuição de referência seja atômica, o compilador pode executar otimizações sob o pressuposto de que a variável não estava sendo modificada por outros threads.
kbolino
1
@BradCupit note que eu disse "se você não estiver usando AtomicReference"; se você estiver usando, meu conselho seria ir na direção oposta e marcá-lo finalpara que o compilador possa otimizar adequadamente.
21715 kbolino
91

Uma referência atômica é ideal para usar quando você precisa compartilhar e alterar o estado de um objeto imutável entre vários encadeamentos. Essa é uma afirmação super densa, então vou descrevê-la um pouco.

Primeiro, um objeto imutável é um objeto que não é efetivamente alterado após a construção. Freqüentemente, os métodos de um objeto imutável retornam novas instâncias dessa mesma classe. Alguns exemplos incluem as classes de wrapper Long e Double, bem como String, apenas para citar alguns. (De acordo com a Programação de simultaneidade nos objetos imutáveis da JVM , é uma parte crítica da simultaneidade moderna).

A seguir, por que o AtomicReference é melhor que um objeto volátil para compartilhar esse valor compartilhado. Um exemplo de código simples mostrará a diferença.

volatile String sharedValue;
static final Object lock=new Object();
void modifyString(){
  synchronized(lock){
    sharedValue=sharedValue+"something to add";
  }
}

Toda vez que você deseja modificar a sequência referenciada por esse campo volátil com base em seu valor atual, primeiro você precisa obter um bloqueio nesse objeto. Isso impede que algum outro encadeamento entre nesse meio tempo e altere o valor no meio da nova concatenação de strings. Então, quando o seu encadeamento continuar, você derruba o trabalho do outro encadeamento. Mas, honestamente, esse código funcionará, parece limpo e faria a maioria das pessoas feliz.

Pequeno problema. Está lento. Especialmente se houver muita contenção nesse objeto de bloqueio. Isso ocorre porque a maioria dos bloqueios exige uma chamada do sistema OS, e seu thread bloqueará e será alternado para fora da CPU para abrir caminho para outros processos.

A outra opção é usar um AtomicRefrence.

public static AtomicReference<String> shared = new AtomicReference<>();
String init="Inital Value";
shared.set(init);
//now we will modify that value
boolean success=false;
while(!success){
  String prevValue=shared.get();
  // do all the work you need to
  String newValue=shared.get()+"lets add something";
  // Compare and set
  success=shared.compareAndSet(prevValue,newValue);
}

Agora, por que isso é melhor? Honestamente, esse código é um pouco menos limpo do que antes. Mas há algo realmente importante que acontece no AtomicRefrence, que é comparar e trocar. É uma única instrução da CPU, não uma chamada do SO, que faz a troca acontecer. Essa é uma única instrução na CPU. E como não há bloqueios, não há mudança de contexto no caso em que o bloqueio é exercido, o que economiza ainda mais tempo!

O problema é que, para AtomicReferences, isso não usa uma chamada .equals (), mas uma comparação == para o valor esperado. Portanto, verifique se o esperado é o objeto real retornado de entrar no loop.

Erik Helleren
fonte
14
Seus dois exemplos se comportam de maneira diferente. Você precisaria fazer um loop workedpara obter a mesma semântica.
CurtainDog
5
Eu acho que você deve inicializar o valor dentro do construtor AtomicReference, caso contrário, outro thread ainda poderá ver o valor nulo antes de chamar shared.set. (A menos que shared.set é executado em um inicializador estático.)
Henno Vermeulen
8
No seu segundo exemplo, no Java 8 você deve usar algo como: shared.updateAndGet ((x) -> (x + "permite adicionar algo")); ... que chamará repetidamente o .compareAndSet até que funcione. Isso é equivalente ao bloco sincronizado que sempre teria êxito. Você precisa garantir que o lambda transmitido seja livre de efeitos colaterais, pois pode ser chamado várias vezes.
quer
2
Não é necessário tornar String volátil sharedValue. O sincronizado (bloqueio) é bom o suficiente para estabelecer o acontecimento antes do relacionamento.
Jai Pandit
2
"... alterar o estado de um objeto imutável" é impreciso aqui, em parte porque, sendo literal, você não pode alterar o estado de um objeto imutável. O exemplo demonstra a alteração da referência de uma instância de objeto imutável para outra. Sei que isso é pedante, mas acho que vale a pena destacar, dada a confusão na lógica do Thread.
Mark Phillips
30

Aqui está um caso de uso para AtomicReference:

Considere esta classe que atua como um intervalo numérico e usa variáveis ​​AtmomicInteger individuais para manter limites numéricos inferiores e superiores.

public class NumberRange {
    // INVARIANT: lower <= upper
    private final AtomicInteger lower = new AtomicInteger(0);
    private final AtomicInteger upper = new AtomicInteger(0);

    public void setLower(int i) {
        // Warning -- unsafe check-then-act
        if (i > upper.get())
            throw new IllegalArgumentException(
                    "can't set lower to " + i + " > upper");
        lower.set(i);
    }

    public void setUpper(int i) {
        // Warning -- unsafe check-then-act
        if (i < lower.get())
            throw new IllegalArgumentException(
                    "can't set upper to " + i + " < lower");
        upper.set(i);
    }

    public boolean isInRange(int i) {
        return (i >= lower.get() && i <= upper.get());
    }
}

Ambos setLower e setUpper são sequências de verificação e ação, mas não usam bloqueio suficiente para torná-las atômicas. Se o intervalo de números for mantido (0, 10) e um thread chamar setLower (5) enquanto outro thread chamar setUpper (4), com algum tempo de azar, ambos passarão nas verificações nos setters e as duas modificações serão aplicadas. O resultado é que o intervalo agora mantém (5, 4) um estado inválido. Portanto, enquanto os AtomicIntegers subjacentes são seguros para threads, a classe composta não é. Isso pode ser corrigido usando um AtomicReference em vez de usar AtomicIntegers individuais para os limites superior e inferior.

public class CasNumberRange {
    // Immutable
    private static class IntPair {
        final int lower;  // Invariant: lower <= upper
        final int upper;

        private IntPair(int lower, int upper) {
            this.lower = lower;
            this.upper = upper;
        }
    }

    private final AtomicReference<IntPair> values = 
            new AtomicReference<IntPair>(new IntPair(0, 0));

    public int getLower() {
        return values.get().lower;
    }

    public void setLower(int lower) {
        while (true) {
            IntPair oldv = values.get();
            if (lower > oldv.upper)
                throw new IllegalArgumentException(
                    "Can't set lower to " + lower + " > upper");
            IntPair newv = new IntPair(lower, oldv.upper);
            if (values.compareAndSet(oldv, newv))
                return;
        }
    }

    public int getUpper() {
        return values.get().upper;
    }

    public void setUpper(int upper) {
        while (true) {
            IntPair oldv = values.get();
            if (upper < oldv.lower)
                throw new IllegalArgumentException(
                    "Can't set upper to " + upper + " < lower");
            IntPair newv = new IntPair(oldv.lower, upper);
            if (values.compareAndSet(oldv, newv))
                return;
        }
    }
}
Binita Bharati
fonte
2
Este artigo é semelhante à sua resposta, mas aborda questões mais complicadas. É interessante! ibm.com/developerworks/java/library/j-jtp04186
LppEdd
20

Você pode usar AtomicReference ao aplicar bloqueios otimistas. Você tem um objeto compartilhado e deseja alterá-lo de mais de 1 thread.

  1. Você pode criar uma cópia do objeto compartilhado
  2. Modifique o objeto compartilhado
  3. Você precisa verificar se o objeto compartilhado ainda é o mesmo de antes - se sim, atualize com a referência da cópia modificada.

Como outro segmento pode ter modificado e / pode modificar entre essas duas etapas. Você precisa fazer isso em uma operação atômica. é aqui que o AtomicReference pode ajudar

HamoriZ
fonte
7

Aqui está um caso de uso muito simples e não tem nada a ver com segurança de encadeamento.

Para compartilhar um objeto entre invocações lambda, AtomicReferenceé uma opção :

public void doSomethingUsingLambdas() {

    AtomicReference<YourObject> yourObjectRef = new AtomicReference<>();

    soSomethingThatTakesALambda(() -> {
        yourObjectRef.set(youObject);
    });

    soSomethingElseThatTakesALambda(() -> {
        YourObject yourObject = yourObjectRef.get();
    });
}

Não estou dizendo que isso seja um bom design ou algo assim (é apenas um exemplo trivial), mas se você tiver o caso em que precisa compartilhar um objeto entre invocações lambda, essa AtomicReferenceé uma opção.

De fato, você pode usar qualquer objeto que contenha uma referência, mesmo uma coleção que tenha apenas um item. No entanto, o AtomicReference é um ajuste perfeito.

Benny Bottema
fonte
6

Eu não vou falar muito. Meus respeitados colegas já deram sua contribuição valiosa. O código de execução completo no final deste blog deve remover qualquer confusão. Trata-se de um assento de filme que reserva um pequeno programa no cenário multiencadeado.

Alguns fatos elementares importantes são os seguintes. 1> Diferentes threads podem apenas conter, por exemplo, variáveis ​​de membro estáticas no espaço de heap. 2> A leitura ou gravação volátil são completamente atômicas e serializadas / acontecem antes e são feitas apenas a partir da memória. Ao dizer isso, quero dizer que qualquer leitura seguirá a gravação anterior na memória. E qualquer gravação seguirá a leitura anterior da memória. Portanto, qualquer encadeamento que trabalhe com um volátil sempre verá o valor mais atualizado. AtomicReference usa essa propriedade de volátil.

A seguir, estão alguns dos códigos-fonte do AtomicReference. AtomicReference refere-se a uma referência de objeto. Essa referência é uma variável de membro volátil na instância AtomicReference, como abaixo.

private volatile V value;

get () simplesmente retorna o valor mais recente da variável (como os voláteis fazem da maneira "acontece antes").

public final V get()

A seguir, é apresentado o método mais importante do AtomicReference.

public final boolean  compareAndSet(V expect, V update) {
        return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
}

O método compareAndSet (expect, update) chama o método compareAndSwapObject () da classe insegura de Java. Essa chamada de método inseguro chama a chamada nativa, que chama uma única instrução para o processador. "expect" e "update" referenciam cada objeto.

Se e somente se a variável de membro da instância AtomicReference "value" se referir ao mesmo objeto for referida por "expect", "update" será atribuída a essa variável de instância agora e "true" será retornado. Ou então, false é retornado. A coisa toda é feita atomicamente. Nenhum outro segmento pode interceptar no meio. Como esta é uma operação de processador único (mágica da arquitetura moderna do computador), geralmente é mais rápida do que usar um bloco sincronizado. Mas lembre-se de que quando várias variáveis ​​precisam ser atualizadas atomicamente, o AtomicReference não ajuda.

Gostaria de adicionar um código de execução completo, que pode ser executado no eclipse. Isso esclareceria muitas confusões. Aqui 22 usuários (threads MyTh) estão tentando reservar 20 lugares. A seguir está o trecho de código seguido pelo código completo.

Snippet de código em que 22 usuários estão tentando reservar 20 vagas.

for (int i = 0; i < 20; i++) {// 20 seats
            seats.add(new AtomicReference<Integer>());
        }
        Thread[] ths = new Thread[22];// 22 users
        for (int i = 0; i < ths.length; i++) {
            ths[i] = new MyTh(seats, i);
            ths[i].start();
        }

A seguir, é apresentado o código completo.

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

public class Solution {

    static List<AtomicReference<Integer>> seats;// Movie seats numbered as per
                                                // list index

    public static void main(String[] args) throws InterruptedException {
        // TODO Auto-generated method stub
        seats = new ArrayList<>();
        for (int i = 0; i < 20; i++) {// 20 seats
            seats.add(new AtomicReference<Integer>());
        }
        Thread[] ths = new Thread[22];// 22 users
        for (int i = 0; i < ths.length; i++) {
            ths[i] = new MyTh(seats, i);
            ths[i].start();
        }
        for (Thread t : ths) {
            t.join();
        }
        for (AtomicReference<Integer> seat : seats) {
            System.out.print(" " + seat.get());
        }
    }

    /**
     * id is the id of the user
     * 
     * @author sankbane
     *
     */
    static class MyTh extends Thread {// each thread is a user
        static AtomicInteger full = new AtomicInteger(0);
        List<AtomicReference<Integer>> l;//seats
        int id;//id of the users
        int seats;

        public MyTh(List<AtomicReference<Integer>> list, int userId) {
            l = list;
            this.id = userId;
            seats = list.size();
        }

        @Override
        public void run() {
            boolean reserved = false;
            try {
                while (!reserved && full.get() < seats) {
                    Thread.sleep(50);
                    int r = ThreadLocalRandom.current().nextInt(0, seats);// excludes
                                                                            // seats
                                                                            //
                    AtomicReference<Integer> el = l.get(r);
                    reserved = el.compareAndSet(null, id);// null means no user
                                                            // has reserved this
                                                            // seat
                    if (reserved)
                        full.getAndIncrement();
                }
                if (!reserved && full.get() == seats)
                    System.out.println("user " + id + " did not get a seat");
            } catch (InterruptedException ie) {
                // log it
            }
        }
    }

}    
sankar banerjee
fonte
5

Quando usamos AtomicReference?

AtomicReference é uma maneira flexível de atualizar o valor da variável atomicamente sem o uso de sincronização.

AtomicReference suporte a programação segura de thread sem bloqueio em variáveis ​​únicas.

Existem várias maneiras de alcançar a segurança do thread com API simultânea de alto nível . Variáveis ​​atômicas são uma das várias opções.

Lock objetos oferecem suporte a idiomas de bloqueio que simplificam muitos aplicativos simultâneos.

Executorsdefina uma API de alto nível para iniciar e gerenciar threads. As implementações de executores fornecidas pelo java.util.concurrent fornecem gerenciamento de conjunto de encadeamentos adequado para aplicativos de grande escala.

Coleções simultâneas facilitam o gerenciamento de grandes coleções de dados e podem reduzir bastante a necessidade de sincronização.

As variáveis ​​atômicas possuem recursos que minimizam a sincronização e ajudam a evitar erros de consistência de memória.

Forneça um exemplo simples em que AtomicReference deve ser usado.

Código de exemplo com AtomicReference:

String initialReference = "value 1";

AtomicReference<String> someRef =
    new AtomicReference<String>(initialReference);

String newReference = "value 2";
boolean exchanged = someRef.compareAndSet(initialReference, newReference);
System.out.println("exchanged: " + exchanged);

É necessário criar objetos em todos os programas multithread?

Você não precisa usar AtomicReferenceem todos os programas multiencadeados.

Se você deseja proteger uma única variável, use AtomicReference. Se você deseja proteger um bloco de código, use outras construções como Lock/ synchronizedetc.

Ravindra babu
fonte
-1

Outro exemplo simples é fazer uma modificação de thread seguro em um objeto de sessão.

public PlayerScore getHighScore() {
    ServletContext ctx = getServletConfig().getServletContext();
    AtomicReference<PlayerScore> holder 
        = (AtomicReference<PlayerScore>) ctx.getAttribute("highScore");
    return holder.get();
}

public void updateHighScore(PlayerScore newScore) {
    ServletContext ctx = getServletConfig().getServletContext();
    AtomicReference<PlayerScore> holder 
        = (AtomicReference<PlayerScore>) ctx.getAttribute("highScore");
    while (true) {
        HighScore old = holder.get();
        if (old.score >= newScore.score)
            break;
        else if (holder.compareAndSet(old, newScore))
            break;
    } 
}

Fonte: http://www.ibm.com/developerworks/library/j-jtp09238/index.html

Dherik
fonte