Como atômico / volátil / sincronizado funciona internamente?
Qual é a diferença entre os seguintes blocos de código?
Código 1
private int counter;
public int getNextUniqueIndex() {
return counter++;
}
Código 2
private AtomicInteger counter;
public int getNextUniqueIndex() {
return counter.getAndIncrement();
}
Código 3
private volatile int counter;
public int getNextUniqueIndex() {
return counter++;
}
Funciona volatile
da seguinte maneira? É
volatile int i = 0;
void incIBy5() {
i += 5;
}
equivalente a
Integer i = 5;
void incIBy5() {
int temp;
synchronized(i) { temp = i }
synchronized(i) { i = temp + 5 }
}
Eu acho que dois threads não podem entrar em um bloco sincronizado ao mesmo tempo ... estou certo? Se isso é verdade, então como atomic.incrementAndGet()
funciona sem synchronized
? E é thread-safe?
E qual é a diferença entre leitura e escrita internas para variáveis voláteis / variáveis atômicas? Li em algum artigo que o thread tem uma cópia local das variáveis - o que é isso?
Respostas:
Você está perguntando especificamente sobre como eles funcionam internamente , então aqui está você:
Sem sincronização
Basicamente, lê o valor da memória, incrementa e coloca de volta na memória. Isso funciona em thread único, mas hoje em dia, na era dos caches de vários núcleos, várias CPUs e vários níveis, não funcionará corretamente. Antes de tudo, ele introduz a condição de corrida (vários threads podem ler o valor ao mesmo tempo), mas também problemas de visibilidade. O valor pode ser armazenado apenas na memória da CPU " local " (algum cache) e não estar visível para outras CPUs / núcleos (e, portanto, - threads). É por isso que muitos se referem à cópia local de uma variável em um encadeamento. É muito inseguro. Considere este código popular, mas quebrado, de parada de encadeamento:
Adicione
volatile
àstopped
variável e ela funcionará bem - se qualquer outro thread modificar astopped
variável por meio dopleaseStop()
método, você terá certeza de ver essa alteração imediatamente nowhile(!stopped)
loop do thread de trabalho . BTW, também não é uma boa maneira de interromper um encadeamento, consulte: Como parar um encadeamento em execução para sempre sem qualquer uso e Interrompendo um encadeamento java específico .AtomicInteger
A
AtomicInteger
classe usa operações de CPU de baixo nível CAS ( comparar e trocar ) (não é necessária sincronização!). Elas permitem modificar uma variável específica apenas se o valor presente for igual a outra coisa (e for retornado com êxito). Portanto, quando você o executa,getAndIncrement()
ele roda em loop (implementação real simplificada):Então basicamente: leia; tente armazenar valor incrementado; se não for bem-sucedido (o valor não é mais igual a
current
), leia e tente novamente. OcompareAndSet()
é implementado no código nativo (assembly).volatile
sem sincronizaçãoEste código não está correto. Ele corrige o problema de visibilidade (
volatile
garante que outros threads possam ver as alterações feitascounter
), mas ainda tem uma condição de corrida. Isso foi explicado várias vezes: o pré / pós-incremento não é atômico.O único efeito colateral de
volatile
" liberar " os caches para que todas as outras partes vejam a versão mais recente dos dados. Isso é muito rigoroso na maioria das situações; é por isso quevolatile
não é padrão.volatile
sem sincronização (2)O mesmo problema acima, mas ainda pior, porque
i
não éprivate
. A condição de corrida ainda está presente. Por que isso é um problema? Se, digamos, dois encadeamentos executam esse código simultaneamente, a saída pode ser+ 5
ou+ 10
. No entanto, você está garantido para ver a alteração.Independente múltiplo
synchronized
Surpresa, esse código também está incorreto. De fato, está completamente errado. Antes de tudo, você está sincronizando
i
, o que está prestes a ser alterado (além disso,i
é um primitivo, então eu acho que você está sincronizando em um temporárioInteger
criado via autoboxing ...) Completamente defeituoso. Você também pode escrever:Não há dois segmentos que podem entrar no mesmo
synchronized
bloco com o mesmo bloqueio . Nesse caso (e da mesma forma em seu código), o objeto de bloqueio é alterado a cada execução e, portanto,synchronized
efetivamente não tem efeito.Mesmo se você tiver usado uma variável final (ou
this
) para sincronização, o código ainda está incorreto. Primeiro, dois threads podem leri
de formatemp
síncrona (tendo o mesmo valor localmentetemp
), depois o primeiro atribui um novo valor ai
(digamos, de 1 a 6) e o outro faz a mesma coisa (de 1 a 6).A sincronização deve abranger desde a leitura até a atribuição de um valor. Sua primeira sincronização não tem efeito (a leitura de um
int
é atômica) e a segunda também. Na minha opinião, estas são as formas corretas:fonte
compareAndSet
é apenas um invólucro fino em torno da operação do CAS. Entro em alguns detalhes na minha resposta.Declarar uma variável como volátil significa que a modificação de seu valor afeta imediatamente o armazenamento de memória real da variável. O compilador não pode otimizar nenhuma referência feita à variável. Isso garante que, quando um thread modifica a variável, todos os outros threads veem o novo valor imediatamente. (Isso não é garantido para variáveis não voláteis.)
Declarar uma variável atômica garante que as operações feitas na variável ocorram de maneira atômica, ou seja, que todas as subetapas da operação sejam concluídas no encadeamento, são executadas e não são interrompidas por outros encadeamentos. Por exemplo, uma operação de incremento e teste exige que a variável seja incrementada e depois comparada com outro valor; uma operação atômica garante que ambas as etapas sejam concluídas como se fossem uma única operação indivisível / ininterrupta.
A sincronização de todos os acessos a uma variável permite que apenas um único encadeamento de cada vez acesse a variável e força todos os outros encadeamentos a aguardar que o encadeamento de acesso libere seu acesso à variável.
O acesso sincronizado é semelhante ao acesso atômico, mas as operações atômicas geralmente são implementadas em um nível mais baixo de programação. Além disso, é inteiramente possível sincronizar apenas alguns acessos a uma variável e permitir que outros acessos não sejam sincronizados (por exemplo, sincronize todas as gravações em uma variável, mas nenhuma das leituras dela).
Atomicidade, sincronização e volatilidade são atributos independentes, mas geralmente são usados em combinação para reforçar a cooperação adequada do encadeamento para acessar variáveis.
Adendo (abril de 2016)
O acesso sincronizado a uma variável geralmente é implementado usando um monitor ou semáforo . Esses são mecanismos mutex de baixo nível (exclusão mútua) que permitem que um thread adquira controle de uma variável ou bloco de código exclusivamente, forçando todos os outros threads a aguardarem se também tentarem adquirir o mesmo mutex. Depois que o encadeamento proprietário libera o mutex, outro encadeamento pode adquirir o mutex por sua vez.
Adenda (julho de 2016)
A sincronização ocorre em um objeto . Isso significa que a chamada de um método sincronizado de uma classe bloqueará o
this
objeto da chamada. Métodos sincronizados estáticos bloquearão oClass
próprio objeto.Da mesma forma, inserir um bloco sincronizado requer o bloqueio do
this
objeto do método.Isso significa que um método (ou bloco) sincronizado pode ser executado em vários threads ao mesmo tempo, se estiver bloqueando objetos diferentes , mas apenas um thread pode executar um método (ou bloco) sincronizado de cada vez para qualquer objeto único .
fonte
volátil:
volatile
é uma palavra-chave.volatile
força todos os threads a obter o valor mais recente da variável da memória principal em vez do cache. Nenhum bloqueio é necessário para acessar variáveis voláteis. Todos os threads podem acessar o valor variável volátil ao mesmo tempo.O uso de
volatile
variáveis reduz o risco de erros de consistência da memória, porque qualquer gravação em uma variável volátil estabelece um relacionamento de antes do acontecimento com leituras subsequentes dessa mesma variável.Isso significa que as alterações em uma
volatile
variável são sempre visíveis para outros threads . Além disso, também significa que, quando um thread lê umavolatile
variável, ele vê não apenas as alterações mais recentes no volátil, mas também os efeitos colaterais do código que levou à alteração .Quando usar: Um thread modifica os dados e outros threads precisam ler o valor mais recente dos dados. Outros threads tomarão alguma ação, mas não atualizarão os dados .
AtomicXXX:
AtomicXXX
As classes suportam a programação segura de thread sem bloqueio em variáveis únicas. EssasAtomicXXX
classes (comoAtomicInteger
) resolvem erros de inconsistência de memória / efeitos colaterais da modificação de variáveis voláteis, que foram acessadas em vários encadeamentos.Quando usar: Vários threads podem ler e modificar dados.
sincronizado:
synchronized
é a palavra-chave usada para proteger um método ou bloco de código. Fazendo o método como sincronizado, tem dois efeitos:Primeiro, não é possível
synchronized
intercalar duas invocações de métodos no mesmo objeto. Quando um encadeamento está executando umsynchronized
método para um objeto, todos os outros encadeamentos que invocamsynchronized
métodos para o mesmo bloco de objetos (suspendem a execução) até que o primeiro encadeamento seja concluído com o objeto.Segundo, quando um
synchronized
método sai, ele estabelece automaticamente um relacionamento de antes do acontecimento com qualquer chamada subseqüente de umsynchronized
método para o mesmo objeto. Isso garante que as alterações no estado do objeto sejam visíveis para todos os threads.Quando usar: Vários threads podem ler e modificar dados. Sua lógica de negócios não apenas atualiza os dados, mas também executa operações atômicas
AtomicXXX
é equivalente,volatile + synchronized
mesmo que a implementação seja diferente.AmtomicXXX
estendevolatile
variáveis +compareAndSet
métodos, mas não usa sincronização.Perguntas relacionadas ao SE:
Diferença entre volátil e sincronizado em Java
AtomicBoolean vs Volatile boolean
Bons artigos para ler: (O conteúdo acima é retirado dessas páginas de documentação)
https://docs.oracle.com/javase/tutorial/essential/concurrency/sync.html
https://docs.oracle.com/javase/tutorial/essential/concurrency/atomic.html
https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/atomic/package-summary.html
fonte
Dois segmentos não podem inserir um bloco sincronizado no mesmo objeto duas vezes. Isso significa que dois threads podem inserir o mesmo bloco em objetos diferentes. Essa confusão pode levar a códigos como este.
Isso não se comportará conforme o esperado, pois poderia estar bloqueando um objeto diferente a cada vez.
sim. Ele não usa travamento para garantir a segurança da linha.
Se você quiser saber como eles funcionam com mais detalhes, leia o código para eles.
A classe atômica usa campos voláteis . Não há diferença no campo. A diferença são as operações realizadas. As classes Atomic usam operações CompareAndSwap ou CAS.
Só posso supor que isso se refere ao fato de que cada CPU possui sua própria visão em cache da memória, que pode ser diferente de qualquer outra CPU. Para garantir que sua CPU tenha uma visão consistente dos dados, você precisa usar técnicas de segurança de encadeamento.
Esse é apenas um problema quando a memória é compartilhada, pelo menos um thread a atualiza.
fonte
Sincronizado vs atômico vs volátil:
Por favor, me corrija se algo que eu perdi.
fonte
Uma sincronização volátil + é uma solução à prova de idiotas para uma operação (instrução) ser totalmente atômica, que inclui várias instruções para a CPU.
Diga por exemplo: volátil int i = 2; i ++, que nada mais é do que i = i + 1; que faz i como o valor 3 na memória após a execução desta instrução. Isso inclui ler o valor existente da memória para i (que é 2), carregar no registro do acumulador da CPU e fazer o cálculo incrementando o valor existente com um (2 + 1 = 3 no acumulador) e, em seguida, gravar novamente esse valor incrementado de volta à memória. Essas operações não são atômicas o suficiente, embora o valor de i seja volátil. Ser volátil garante apenas que uma ÚNICA leitura / gravação da memória seja atômica e não com MÚLTIPLO. Portanto, precisamos ter sincronizado também em torno do i ++ para mantê-lo como uma declaração atômica à prova de idiotas. Lembre-se do fato de que uma declaração inclui várias declarações.
Espero que a explicação seja clara o suficiente.
fonte
O modificador volátil Java é um exemplo de um mecanismo especial para garantir que a comunicação ocorra entre os encadeamentos. Quando um thread grava em uma variável volátil, e outro vê essa gravação, o primeiro thread informa ao segundo sobre todo o conteúdo da memória até executar a gravação nessa variável volátil.
As operações atômicas são executadas em uma única unidade de tarefa sem interferência de outras operações. As operações atômicas são necessárias no ambiente multithread para evitar inconsistência de dados.
fonte