É importante entender que existem dois aspectos para a segurança do encadeamento.
- controle de execução e
- visibilidade da memória
O primeiro tem a ver com o controle de quando o código é executado (incluindo a ordem na qual as instruções são executadas) e se ele pode ser executado simultaneamente, e o segundo com o momento em que os efeitos na memória do que foi feito são visíveis para outros threads. Como cada CPU possui vários níveis de cache entre ela e a memória principal, os segmentos em execução em diferentes CPUs ou núcleos podem ver a "memória" de maneira diferente a qualquer momento, porque os segmentos têm permissão para obter e trabalhar em cópias particulares da memória principal.
O uso synchronized
impede que qualquer outro encadeamento obtenha o monitor (ou bloqueio) para o mesmo objeto , impedindo a execução simultânea de todos os blocos de código protegidos pela sincronização no mesmo objeto . A sincronização também cria uma barreira de memória "acontece antes", causando uma restrição de visibilidade da memória, de modo que qualquer coisa feita até o ponto em que um thread libera um bloqueio apareça em outro segmento, adquirindo posteriormente o mesmo bloqueio que ocorreu antes de o bloqueio ser adquirido. Em termos práticos, no hardware atual, isso geralmente causa a liberação dos caches da CPU quando um monitor é adquirido e grava na memória principal quando é lançado, sendo ambos (relativamente) caros.
O uso volatile
, por outro lado, força todos os acessos (leitura ou gravação) à variável volátil a ocorrerem na memória principal, mantendo efetivamente a variável volátil fora dos caches da CPU. Isso pode ser útil para algumas ações em que é simplesmente necessário que a visibilidade da variável esteja correta e a ordem dos acessos não seja importante. O uso volatile
também altera o tratamento de long
e ). Para fins de visibilidade, cada acesso a um campo volátil atua como meia sincronização.double
exige que os acessos sejam atômicos; em alguns hardwares (mais antigos), isso pode exigir bloqueios, embora não no hardware moderno de 64 bits. Sob o novo modelo de memória (JSR-133) para Java 5+, a semântica do volátil foi reforçada para ser quase tão forte quanto a sincronizada em relação à visibilidade da memória e à ordem das instruções (consulte http://www.cs.umd.edu /users/pugh/java/memoryModel/jsr-133-faq.html#volatile
Sob o novo modelo de memória, ainda é verdade que variáveis voláteis não podem ser reordenadas entre si. A diferença é que agora não é mais tão fácil reordenar acessos de campo normais ao seu redor. Gravar em um campo volátil tem o mesmo efeito de memória que uma liberação do monitor, e a leitura de um campo volátil tem o mesmo efeito de memória que uma aquisição do monitor. Com efeito, como o novo modelo de memória impõe restrições mais rígidas à reordenação de acessos voláteis de campo com outros acessos voláteis, voláteis ou não, qualquer coisa visível ao encadeamento A
quando ele grava no campo volátil f
se torna visível ao encadeamento B
quando lê f
.
- Perguntas frequentes sobre JSR 133 (Java Memory Model)
Portanto, agora as duas formas de barreira à memória (sob o JMM atual) causam uma barreira para reordenar as instruções que impede que o compilador ou o tempo de execução reordenem as instruções através da barreira. No antigo JMM, o volátil não impediu o pedido novamente. Isso pode ser importante, porque, além das barreiras de memória, a única limitação imposta é que, para qualquer thread em particular , o efeito líquido do código seja o mesmo que seria se as instruções fossem executadas exatamente na ordem em que aparecem no fonte.
Um uso de volátil é para um objeto compartilhado, mas imutável, ser recriado em tempo real, com muitos outros threads fazendo referência ao objeto em um ponto específico do ciclo de execução. É necessário que os outros encadeamentos comecem a usar o objeto recriado após a publicação, mas não precisam da sobrecarga adicional da sincronização completa e de sua contenção e liberação do cache.
// Declaration
public class SharedLocation {
static public SomeObject someObject=new SomeObject(); // default object
}
// Publishing code
// Note: do not simply use SharedLocation.someObject.xxx(), since although
// someObject will be internally consistent for xxx(), a subsequent
// call to yyy() might be inconsistent with xxx() if the object was
// replaced in between calls.
SharedLocation.someObject=new SomeObject(...); // new object is published
// Using code
private String getError() {
SomeObject myCopy=SharedLocation.someObject; // gets current copy
...
int cod=myCopy.getErrorCode();
String txt=myCopy.getErrorText();
return (cod+" - "+txt);
}
// And so on, with myCopy always in a consistent state within and across calls
// Eventually we will return to the code that gets the current SomeObject.
Falando especificamente sobre sua pergunta de leitura, atualização e gravação. Considere o seguinte código não seguro:
public void updateCounter() {
if(counter==1000) { counter=0; }
else { counter++; }
}
Agora, com o método updateCounter () não sincronizado, dois threads podem inseri-lo ao mesmo tempo. Entre as muitas permutações do que poderia acontecer, uma é que o thread-1 faz o teste para o contador == 1000 e descobre que é verdadeiro e é suspenso. Em seguida, o thread-2 faz o mesmo teste e também o considera verdadeiro e está suspenso. Em seguida, o thread-1 continua e define o contador como 0. Em seguida, o thread-2 continua e define o contador novamente como 0 porque perdeu a atualização do thread-1. Isso também pode acontecer mesmo que a troca de encadeamento não ocorra como descrevi, mas simplesmente porque duas cópias em cache diferentes do contador estavam presentes em dois núcleos de CPU diferentes e os encadeamentos eram executados em um núcleo separado. Nesse caso, um encadeamento pode ter contador em um valor e o outro pode ter contador em algum valor completamente diferente apenas por causa do armazenamento em cache.
O importante neste exemplo é que o contador de variáveis foi lido da memória principal no cache, atualizado no cache e gravado apenas na memória principal em algum momento indeterminado posteriormente, quando ocorreu uma barreira de memória ou quando a memória cache era necessária para outra coisa. Fazer o contador volatile
é insuficiente para a segurança do encadeamento desse código, porque o teste para o máximo e as atribuições são operações discretas, incluindo o incremento, que é um conjunto de read+increment+write
instruções não atômicas da máquina, algo como:
MOV EAX,counter
INC EAX
MOV counter,EAX
Variáveis voláteis são úteis apenas quando todas as operações executadas nelas são "atômicas", como no meu exemplo, em que uma referência a um objeto totalmente formado é apenas lida ou gravada (e, de fato, normalmente é escrita apenas a partir de um único ponto). Outro exemplo seria uma referência de matriz volátil apoiando uma lista de cópia na gravação, desde que a matriz fosse lida apenas pela primeira cópia local da referência.
http://javaexp.blogspot.com/2007/12/difference-between-volatile-and.html
fonte
synchronized
é um modificador de restrição de acesso no nível do método / nível do bloco. Ele garantirá que um thread possua o bloqueio da seção crítica. Somente a linha que possui uma trava pode entrar nosynchronized
bloco. Se outros threads estiverem tentando acessar esta seção crítica, terão que esperar até que o proprietário atual libere o bloqueio.volatile
é um modificador de acesso variável que força todos os threads a obter o valor mais recente da variável da memória principal. Nenhum bloqueio é necessário para acessarvolatile
variáveis. Todos os threads podem acessar o valor variável volátil ao mesmo tempo.Um bom exemplo para usar a variável volátil:
Date
variable.Suponha que você tenha feito a variável Data
volatile
. Todos os threads que acessam essa variável sempre obtêm os dados mais recentes da memória principal, de modo que todos os threads mostrem o valor real (real) da data. Você não precisa de threads diferentes mostrando um tempo diferente para a mesma variável. Todos os threads devem mostrar o valor correto da data.Dê uma olhada neste artigo para entender melhor o
volatile
conceito.Lawrence Dol Cleary explicou o seu
read-write-update query
.Em relação às suas outras consultas
Você tem que usar
volatile
se acha que todos os threads devem obter o valor real da variável em tempo real, como no exemplo que expliquei para a variável Date.A resposta será a mesma da primeira consulta.
Consulte este artigo para entender melhor.
fonte
tl; dr :
Existem três problemas principais com o multithreading:
1) Condições da corrida
2) Memória em cache / obsoleta
3) Otimizações completas e de CPU
volatile
pode resolver 2 e 3, mas não pode resolver 1.synchronized
/ bloqueios explícitos podem resolver 1, 2 e 3.Elaboração :
1) Considere este código não seguro de thread:
x++;
Embora possa parecer uma operação, na verdade é 3: lendo o valor atual de x da memória, adicionando 1 a ele e salvando-o na memória. Se alguns encadeamentos tentarem fazê-lo ao mesmo tempo, o resultado da operação será indefinido. Se
x
originalmente era 1, após 2 threads operando o código, pode ser 2 e 3, dependendo de qual thread concluiu qual parte da operação antes do controle foi transferido para o outro thread. Esta é uma forma de condição de corrida .Usar
synchronized
um bloco de código o torna atômico - o que significa que é como se as três operações acontecessem ao mesmo tempo, e não há como outro encadeamento entrar no meio e interferir. Então, sex
foi 1 e 2 threads tentam pré-formax++
, sabemos que no final será igual a 3. Portanto, resolve o problema de condição de corrida.Marcar
x
comovolatile
não tornax++;
atômico, portanto, não resolve esse problema.2) Além disso, os threads têm seu próprio contexto - ou seja, eles podem armazenar em cache valores da memória principal. Isso significa que alguns threads podem ter cópias de uma variável, mas operam em sua cópia de trabalho sem compartilhar o novo estado da variável entre outros threads.
Considere isso em um tópico
x = 10;
,. E um pouco mais tarde, em outro tópicox = 20;
,. A alteração no valor dex
pode não aparecer no primeiro thread, porque o outro thread salvou o novo valor na memória de trabalho, mas não o copiou na memória principal. Ou copiou-o para a memória principal, mas o primeiro thread não atualizou sua cópia de trabalho. Portanto, se agora o primeiro thread verificar,if (x == 20)
a resposta seráfalse
.Marcar uma variável como
volatile
basicamente diz a todos os threads para executar operações de leitura e gravação apenas na memória principal.synchronized
diz a cada thread para atualizar seu valor da memória principal quando entrar no bloco e liberar o resultado de volta para a memória principal quando sair do bloco.Observe que, diferentemente das corridas de dados, a memória obsoleta não é tão fácil de (re) produzir, pois as descargas na memória principal ocorrem de qualquer maneira.
3) O complier e a CPU podem (sem qualquer forma de sincronização entre os threads) tratar todo o código como um único thread. Isso significa que ele pode olhar para algum código, que é muito significativo em um aspecto de multithreading, e tratá-lo como se fosse um encadeamento único, onde não é tão significativo. Portanto, ele pode examinar um código e decidir, com o objetivo de otimização, reordená-lo ou até mesmo remover partes dele completamente, se não souber que esse código foi projetado para funcionar em vários encadeamentos.
Considere o seguinte código:
Você pensaria que o threadB só poderia imprimir 20 (ou não imprimir nada se a verificação de threadB for executada antes de ser configurada
b
como true), comob
é definido como true somente depoisx
de 20, mas o compilador / CPU pode decidir reordenar threadA, nesse caso threadB também pode imprimir 10. Marcaçãob
comovolatile
garante que ele não vai ser reordenadas (ou descartados em certos casos). Qual threadB médio pode imprimir apenas 20 (ou nada). Marcar os métodos como sincronizados alcançará o mesmo resultado. A marcação de uma variável tambémvolatile
garante que ela não seja reordenada, mas tudo antes / depois dela ainda pode ser reordenada, para que a sincronização seja mais adequada em alguns cenários.Observe que antes do Java 5 New Memory Model, o volátil não solucionava esse problema.
fonte
INC
operação de montagem , as operações subjacentes da CPU ainda são 3 vezes necessárias e exigem travamento para segurança do encadeamento. Bom ponto. Embora, osINC/DEC
comandos possam ser sinalizados atômica na montagem e ainda serem uma operação atômica.