Qual é a diferença entre atômico / volátil / sincronizado?

297

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 volatileda 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?

hardik
fonte
5
Isso faz muitas perguntas, com código que nem compila. Talvez você devesse ler um bom livro, como o Java Concurrency in Practice.
JB Nizet
4
@JBNizet você está certo !!! eu tenho esse livro, ele não tem o conceito Atomic em resumo e eu não estou entendendo alguns conceitos disso. de maldição é meu erro, não de autor.
hardik
4
Você realmente não precisa se preocupar com a implementação (e isso varia com o sistema operacional). O que você precisa entender é o contrato: o valor é incrementado atomicamente e todos os outros encadeamentos são garantidos para ver o novo valor.
JB Nizet

Respostas:

392

Você está perguntando especificamente sobre como eles funcionam internamente , então aqui está você:

Sem sincronização

private int counter;

public int getNextUniqueIndex() {
  return counter++; 
}

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:

private boolean stopped;

public void run() {
    while(!stopped) {
        //do some work
    }
}

public void pleaseStop() {
    stopped = true;
}

Adicione volatileà stoppedvariável e ela funcionará bem - se qualquer outro thread modificar a stoppedvariável por meio do pleaseStop()método, você terá certeza de ver essa alteração imediatamente no while(!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

private AtomicInteger counter = new AtomicInteger();

public int getNextUniqueIndex() {
  return counter.getAndIncrement();
}

A AtomicIntegerclasse 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):

int current;
do {
  current = get();
} while(!compareAndSet(current, current + 1));

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. O compareAndSet()é implementado no código nativo (assembly).

volatile sem sincronização

private volatile int counter;

public int getNextUniqueIndex() {
  return counter++; 
}

Este código não está correto. Ele corrige o problema de visibilidade ( volatilegarante que outros threads possam ver as alterações feitas counter), 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 que volatilenão é padrão.

volatile sem sincronização (2)

volatile int i = 0;
void incIBy5() {
  i += 5;
}

O mesmo problema acima, mas ainda pior, porque inã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 + 5ou + 10. No entanto, você está garantido para ver a alteração.

Independente múltiplo synchronized

void incIBy5() {
  int temp;
  synchronized(i) { temp = i }
  synchronized(i) { i = temp + 5 }
}

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ário Integercriado via autoboxing ...) Completamente defeituoso. Você também pode escrever:

synchronized(new Object()) {
  //thread-safe, SRSLy?
}

Não há dois segmentos que podem entrar no mesmo synchronizedbloco 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, synchronizedefetivamente 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 ler ide forma tempsíncrona (tendo o mesmo valor localmente temp), depois o primeiro atribui um novo valor a i(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:

void synchronized incIBy5() {
  i += 5 
}

void incIBy5() {
  synchronized(this) {
    i += 5 
  }
}

void incIBy5() {
  synchronized(this) {
    int temp = i;
    i = temp + 5;
  }
}
Tomasz Nurkiewicz
fonte
10
A única coisa que eu acrescentaria é que a JVM copia valores variáveis ​​em registradores para operá-los. Isso significa que os threads em execução em uma única CPU / núcleo ainda podem ver valores diferentes para uma variável não volátil.
David Harkness
@ thomasz: compareAndSet (atual, atual + 1) sincronizado? se não, o que acontece quando dois threads estão executando este método ao mesmo tempo?
hardik
@ Hardik: compareAndSeté apenas um invólucro fino em torno da operação do CAS. Entro em alguns detalhes na minha resposta.
Tomasz Nurkiewicz 02/04/12
1
@ thomsasz: ok, eu passo por essa pergunta de link e respondida por jon skeet, ele diz que "o thread não consegue ler uma variável volátil sem verificar se algum outro thread realizou uma gravação". mas o que acontece se um thread estiver no meio da operação de gravação e o segundo thread estiver lendo-o !! estou errado ?? não é condição de corrida em operação atômica?
hardik
3
@ Hardik: por favor, crie outra pergunta para obter mais respostas sobre o que você está perguntando, aqui somos apenas você e eu e os comentários não são adequados para fazer perguntas. Não se esqueça de postar um link para uma nova pergunta aqui para que eu possa acompanhar.
Tomasz Nurkiewicz 02/04
61

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 thisobjeto da chamada. Métodos sincronizados estáticos bloquearão o Classpróprio objeto.

Da mesma forma, inserir um bloco sincronizado requer o bloqueio do thisobjeto 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 .

David R Tribble
fonte
25

volátil:

volatileé uma palavra-chave. volatileforç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 volatilevariá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 volatilevariável são sempre visíveis para outros threads . Além disso, também significa que, quando um thread lê uma volatilevariá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:

AtomicXXXAs classes suportam a programação segura de thread sem bloqueio em variáveis ​​únicas. Essas AtomicXXXclasses (como AtomicInteger) 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:

  1. Primeiro, não é possível synchronizedintercalar duas invocações de métodos no mesmo objeto. Quando um encadeamento está executando um synchronizedmétodo para um objeto, todos os outros encadeamentos que invocam synchronizedmétodos para o mesmo bloco de objetos (suspendem a execução) até que o primeiro encadeamento seja concluído com o objeto.

  2. Segundo, quando um synchronizedmétodo sai, ele estabelece automaticamente um relacionamento de antes do acontecimento com qualquer chamada subseqüente de um synchronizedmé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 + synchronizedmesmo que a implementação seja diferente. AmtomicXXXestende volatilevariáveis ​​+ compareAndSetmé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

Ravindra babu
fonte
2
Esta é a primeira resposta que realmente menciona a semântica de antes das palavras-chave / recursos descritos, que são importantes para entender como elas realmente afetam a execução do código. Respostas votadas mais altas perdem esse aspecto.
jhyot
5

Eu sei que dois segmentos não podem entrar no bloco Sincronizar ao mesmo tempo

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.

private Integer i = 0;

synchronized(i) {
   i++;
}

Isso não se comportará conforme o esperado, pois poderia estar bloqueando um objeto diferente a cada vez.

se isso for verdadeiro, como Como este atomic.incrementAndGet () funciona sem Sincronizar? e é thread safe ??

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.

E qual é a diferença entre leitura e escrita interna para Volatile Variable / Atomic Variable ??

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.

eu li em algum artigo que o segmento tem cópia local de variáveis ​​o que é isso?

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.

Peter Lawrey
fonte
@Aniket Thakur, você tem certeza disso? Inteiro é imutável. Portanto, o i ++ provavelmente desmarca automaticamente o valor int, incrementa-o e cria um novo número inteiro, que não é a mesma instância de antes. Tente fazer i final e você receberá erros do compilador ao chamar o i ++.
fuemf5
2

Sincronizado vs atômico vs volátil:

  • Volátil e Atômico é aplicável apenas à variável, enquanto Sincronizado se aplica ao método
  • Volátil garante visibilidade e não atomicidade / consistência do objeto, enquanto outros garantem visibilidade e atomicidade.
  • Armazenamento variável volátil na RAM e acesso mais rápido, mas não podemos obter segurança ou sincronização do thread sem a palavra-chave sincronizada.
  • Sincronizado implementado como bloco sincronizado ou método sincronizado, enquanto ambos não. Podemos encadear várias linhas de código seguras com a ajuda de palavras-chave sincronizadas, enquanto que com as duas não conseguimos o mesmo.
  • Sincronizado pode bloquear o mesmo objeto de classe ou objeto de classe diferente, enquanto ambos não podem.

Por favor, me corrija se algo que eu perdi.

Ajay Gupta
fonte
1

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.

Thomas Mathew
fonte
1

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.

Sai prateek
fonte