Diferença entre volátil e sincronizado em Java

233

Gostaria de saber a diferença entre declarar uma variável como volatilee sempre acessar a variável em umsynchronized(this) bloco em Java?

De acordo com este artigo http://www.javamex.com/tutorials/synchronization_volatile.shtml, há muito a ser dito e há muitas diferenças, mas também algumas semelhanças.

Estou particularmente interessado nesta informação:

...

  • o acesso a uma variável volátil nunca tem o potencial de bloquear: estamos apenas fazendo uma simples leitura ou gravação, portanto, diferentemente de um bloco sincronizado, nunca manteremos nenhum bloqueio;
  • como o acesso a uma variável volátil nunca retém um bloqueio, não é adequado para casos em que queremos ler-atualizar-gravar como uma operação atômica (a menos que estejamos preparados para "perder uma atualização");

O que eles querem dizer com leitura-atualização-gravação ? Uma gravação também não é uma atualização ou eles simplesmente significam que a atualização é uma gravação que depende da leitura?

Acima de tudo, quando é mais adequado declarar variáveis ​​em volatilevez de acessá-las através de um synchronizedbloco? É uma boa ideia usar volatilepara variáveis ​​que dependem de entrada? Por exemplo, existe uma variável chamada renderque é lida através do loop de renderização e definida por um evento de pressionamento de tecla?

Albus Dumbledore
fonte

Respostas:

383

É importante entender que existem dois aspectos para a segurança do encadeamento.

  1. controle de execução e
  2. 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 synchronizedimpede 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 volatiletambém altera o tratamento de longe ). 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 Aquando ele grava no campo volátil fse torna visível ao encadeamento Bquando 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+writeinstruçõ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.

Lawrence Dol
fonte
5
Muito obrigado! O exemplo com o contador é simples de entender. No entanto, quando as coisas se realizam, é um pouco diferente.
Albus Dumbledore
"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, os quais são caros (relativamente falando)." . Quando você diz caches de CPU, é o mesmo que Java Stacks local para cada thread? ou um segmento tem sua própria versão local do Heap? Desculpe-me se estou sendo bobo aqui.
NishM 15/09/15
1
@nishm Não é o mesmo, mas incluiria os caches locais dos threads envolvidos. .
Lawrence Dol
1
@ MarianPaździoch: Um incremento ou decréscimo NÃO é uma leitura ou gravação, é uma leitura e uma gravação; é uma leitura em um registro, um incremento de registro e uma gravação de volta à memória. As leituras e gravações são atômicas individualmente , mas várias operações desse tipo não são.
Lawrence Dol
2
Portanto, de acordo com a FAQ, não apenas as ações realizadas desde a aquisição de um bloqueio são visíveis após o desbloqueio, mas também todas as ações realizadas por esse encadeamento. Mesmo ações realizadas antes da aquisição do bloqueio.
Lii
97

volátil é um modificador de campo , enquanto sincronizado modifica blocos e métodos de código . Portanto, podemos especificar três variações de um acessador simples usando essas duas palavras-chave:

    int i1;
    int geti1() {return i1;}

    volatile int i2;
    int geti2() {return i2;}

    int i3;
    synchronized int geti3() {return i3;}

geti1()acessa o valor atualmente armazenado no i1encadeamento atual. Os encadeamentos podem ter cópias locais de variáveis ​​e os dados não precisam ser os mesmos que os contidos em outros encadeamentos. Em particular, outro encadeamento pode ter sido atualizado i1em seu encadeamento, mas o valor no encadeamento atual pode ser diferente daquele valor atualizado. De fato, Java tem a idéia de uma memória "principal", e essa é a memória que mantém o valor "correto" atual para as variáveis. Os encadeamentos podem ter sua própria cópia de dados para variáveis ​​e a cópia do encadeamento pode ser diferente da memória "principal". Portanto, é possível que a memória "principal" tenha um valor de 1 para i1, para o thread1 tenha um valor de 2 para i1e para o thread2ter um valor de 3 para i1se o thread1 e o thread2 tiverem atualizado i1, mas esse valor atualizado ainda não foi propagado para a memória "principal" ou outros encadeamentos.

Por outro lado, geti2()acessa efetivamente o valor da i2memória "principal". Uma variável volátil não pode ter uma cópia local de uma variável diferente do valor atualmente mantido na memória "principal". Efetivamente, uma variável declarada volátil deve ter seus dados sincronizados em todos os segmentos, para que sempre que você acessar ou atualizar a variável em qualquer segmento, todos os outros segmentos vejam imediatamente o mesmo valor. As variáveis ​​geralmente voláteis têm um maior acesso e atualização de sobrecarga do que as variáveis ​​"simples". Geralmente, os threads podem ter sua própria cópia de dados para obter melhor eficiência.

Existem duas diferenças entre volátil e sincronizado.

Primeiramente sincronizado obtém e libera bloqueios em monitores que podem forçar apenas um thread por vez a executar um bloco de código. Esse é o aspecto bastante conhecido para sincronizar. Mas sincronizado também sincroniza a memória. De fato, sincronizado sincroniza toda a memória do thread com a memória "principal". Portanto, a execução geti3()faz o seguinte:

  1. O encadeamento adquire a trava no monitor para o objeto this.
  2. A memória do thread libera todas as suas variáveis, ou seja, possui todas as suas variáveis ​​efetivamente lidas da memória "principal".
  3. O bloco de código é executado (neste caso, definindo o valor de retorno para o valor atual de i3, que pode ter sido redefinido da memória "principal").
  4. (Qualquer alteração nas variáveis ​​normalmente seria gravada na memória "principal", mas para geti3 () não temos alterações.)
  5. O encadeamento libera a trava no monitor para o objeto this.

Portanto, quando volátil sincroniza apenas o valor de uma variável entre a memória de encadeamento e a memória "principal", sincronizado sincroniza o valor de todas as variáveis ​​entre a memória de encadeamento e a memória "principal" e bloqueia e libera um monitor para inicializar. Claramente sincronizado é provável que tenha mais sobrecarga do que volátil.

http://javaexp.blogspot.com/2007/12/difference-between-volatile-and.html

Kerem Baydoğan
fonte
35
-1, o Volatile não adquire um bloqueio, usa a arquitetura subjacente da CPU para garantir a visibilidade de todos os threads após a gravação.
Michael Barker
Vale a pena notar que pode haver alguns casos em que um bloqueio pode ser usado para garantir a atomicidade das gravações. Por exemplo, escrever um longo em uma plataforma de 32 bits que não suporta direitos de largura estendida. A Intel evita isso usando registros SSE2 (128 bits de largura) para lidar com comprimentos voláteis. No entanto, considerar um volátil como um bloqueio provavelmente levará a erros desagradáveis ​​no seu código.
Michael Barker
2
A importante semântica compartilhada por bloqueia uma variável volátil é que ambas fornecem arestas do Happens-Before (Java 1.5 e posterior). A entrada de um bloco sincronizado, a remoção de um bloqueio e a leitura de um volátil são todos considerados como uma "aquisição" e a liberação de um bloqueio, a saída de um bloco sincronizado e a gravação de um volátil são todas as formas de um "release".
Michael Barker
20

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 no synchronizedbloco. 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 acessar volatilevariá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: Datevariable.

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.

insira a descrição da imagem aqui

Dê uma olhada neste artigo para entender melhor o volatileconceito.

Lawrence Dol Cleary explicou o seu read-write-update query.

Em relação às suas outras consultas

Quando é mais adequado declarar variáveis ​​voláteis do que acessá-las através de sincronização?

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.

É uma boa idéia usar volátil para variáveis ​​que dependem de entrada?

A resposta será a mesma da primeira consulta.

Consulte este artigo para entender melhor.

Ravindra babu
fonte
Portanto, a leitura pode acontecer ao mesmo tempo, e todo o encadeamento lerá o valor mais recente porque a CPU não armazena em cache a memória principal no cache de encadeamento da CPU, mas e a gravação? A gravação não deve ser simultânea correta? Segunda pergunta: se um bloco é sincronizado, mas a variável não é volátil, o valor de uma variável em um bloco sincronizado ainda pode ser alterado por outro thread em outro bloco de código, certo?
the_prole 6/01
11

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

volatilepode 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 xoriginalmente 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 synchronizedum 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, se xfoi 1 e 2 threads tentam pré-forma x++, sabemos que no final será igual a 3. Portanto, resolve o problema de condição de corrida.

synchronized (this) {
   x++; // no problem now
}

Marcar xcomo volatilenão torna x++;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ópico x = 20;,. A alteração no valor de xpode 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 volatilebasicamente diz a todos os threads para executar operações de leitura e gravação apenas na memória principal. synchronizeddiz 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:

boolean b = false;
int x = 10;

void threadA() {
    x = 20;
    b = true;
}

void threadB() {
    if (b) {
        System.out.println(x);
    }
}

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 bcomo true), como bé definido como true somente depois xde 20, mas o compilador / CPU pode decidir reordenar threadA, nesse caso threadB também pode imprimir 10. Marcação bcomo volatilegarante 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ém volatilegarante 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.

David Refaeli
fonte
1
"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." - Certo, porque os valores da memória devem passar pelo circuito da CPU para serem adicionados / modificados. Mesmo que isso se transforme em uma única INCoperaçã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, os INC/DECcomandos possam ser sinalizados atômica na montagem e ainda serem uma operação atômica.
Zombies
@ Zombies, então quando eu crio um bloco sincronizado para x ++, ele o transforma em um INC / DEC atômico sinalizado ou usa um bloqueio regular?
David Refaeli
Eu não sei! O que eu sei é que o INC / DEC não é atômico, porque para uma CPU, ele precisa carregar o valor e LER e ESCREVER (gravá-lo (na memória)), como qualquer outra operação aritmética.
Zombies