Diferença entre volátil e sincronizado em Java

Eu estou querendo saber a diferença entre declarar uma variável como volatile e sempre acessando a variável em um bloco synchronized(this) em Java?

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

Estou particularmente interessado nesta informação:

  • o access a uma variável volátil nunca tem o potencial de bloquear: estamos apenas fazendo uma leitura ou gravação simples, assim, ao contrário de um bloco sincronizado, nunca vamos nos prender a nenhum bloqueio;
  • Como acessar uma variável volátil nunca contém um bloqueio, ela não é adequada 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 read-update-write ? Não é uma gravação também 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 variables volatile vez de acessá-las através de um bloco synchronized ? É uma boa ideia usar o volatile para variables ​​que dependem de input? Por exemplo, existe uma variável chamada render que é lida através do loop de renderização e definida por um evento keypress?

É importante entender que há dois aspectos para a segurança de threads.

  1. controle de execução e
  2. visibilidade da memory

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 para saber quando os efeitos na memory do que foi feito são visíveis para outros threads. Como cada processador possui vários níveis de cache entre ele e a memory principal, os threads em execução em diferentes CPUs ou núcleos podem ver “memory” diferentemente em qualquer momento, porque os threads podem obter e trabalhar em cópias privadas da memory principal.

Usar synchronized impede que qualquer outro segmento obtenha o monitor (ou bloqueio) para o mesmo object , evitando assim que todos os blocos de código protegidos pela synchronization no mesmo object sejam executados simultaneamente. A synchronization também cria uma barreira de memory “acontece antes”, causando uma restrição de visibilidade de memory de tal forma que qualquer coisa feita até o momento em que um thread libera um bloqueio aparece para outro thread adquirindo o mesmo bloqueio antes de adquirir o bloqueio. Em termos práticos, no hardware atual, isso normalmente faz com que o cache da CPU seja liberado quando um monitor é adquirido e grava na memory principal quando é liberado, sendo ambos (relativamente) caros.

O uso de volatile , por outro lado, força todos os accesss (ler ou gravar) para a variável volátil a ocorrer na memory principal, efetivamente mantendo 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 que a ordem dos accesss não seja importante. O uso de volatile também altera o tratamento de long e double para exigir que os accesss sejam atômicos; em alguns hardwares (mais antigos) isso pode exigir bloqueios, embora não em hardware de 64 bits moderno. Sob o novo modelo de memory (JSR-133) para o Java 5+, a semântica do volátil foi fortalecida para ser quase tão forte quanto sincronizada em relação à visibilidade da memory e à ordenação de instruções (veja http://www.cs.umd.edu /users/pugh/java/memoryModel/jsr-133-faq.html#volatile ). Para fins de visibilidade, cada access a um campo volátil age como meia synchronization.

Sob o novo modelo de memory, ainda é verdade que as variables ​​voláteis não podem ser reordenadas entre si. A diferença é que agora não é mais tão fácil reordenar os accesss normais de campo em torno deles. Escrever em um campo volátil tem o mesmo efeito de memory que o lançamento de um monitor, e a leitura de um campo volátil tem o mesmo efeito de memory que um monitor adquire. Com efeito, como o novo modelo de memory impõe restrições mais rigorosas à reordenação de accesss de campo voláteis com outros accesss de campo, volátil ou não, qualquer coisa visível para o thread A quando ele grava no campo volátil se torna visível para o thread B quando ele lê f .

– FAQ JSR 133 (Java Memory Model)

Portanto, agora ambas as formas de barreira de memory (sob o JMM atual) causam uma barreira de reordenamento de instruções que impede o compilador ou o tempo de execução de reordenar as instruções através da barreira. No antigo JMM, o volátil não impedia a reordenação. Isso pode ser importante, porque além das barreiras de memory, a única limitação imposta é que, para qualquer thread em particular , o efeito líquido do código é o mesmo que seria se as instruções fossem executadas precisamente na ordem em que aparecem no arquivo. fonte.

Um uso de volatile é que um object compartilhado, mas imutável, é recriado em tempo real, com muitos outros threads tomando uma referência ao object em um determinado ponto em seu ciclo de execução. É necessário que os outros threads comecem a usar o object recriado depois de publicado, mas não precisa da sobrecarga adicional de synchronization completa e sua contenção de atendente e liberação de 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 em sua pergunta read-update-write, especificamente. 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 segmentos 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 o considera verdadeiro e é então suspenso. Em seguida, o thread-2 faz o mesmo teste e também o considera verdadeiro e está suspenso. Em seguida, o thread-1 é retomado e define o contador como 0. Em seguida, o thread-2 é retomado e novamente define o contador como 0 porque perdeu a atualização do thread-1. Isso também pode acontecer mesmo se a troca de encadeamento não ocorrer como eu descrevi, mas simplesmente porque duas cópias de contador em cache diferentes estavam presentes em dois núcleos de CPU diferentes e cada encadeamento foi executado em um núcleo separado. Aliás, um thread poderia ter um contador em um valor e o outro poderia ter um valor totalmente diferente por causa do cache.

O que é importante neste exemplo é que o contador de variables ​​foi lido da memory principal para o cache, atualizado em cache e gravado apenas na memory principal em algum ponto indeterminado depois quando ocorreu uma barreira de memory ou quando a memory cache era necessária para outra coisa. Tornar o contador volatile é insuficiente para segurança de thread deste 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 instruções não-atômicas read+increment+write machine, algo como:

 MOV EAX,counter INC EAX MOV counter,EAX 

As variables ​​voláteis são úteis somente quando todas as operações executadas nelas são “atômicas”, como meu exemplo, em que uma referência a um object totalmente formado é somente lida ou escrita (e, na verdade, normalmente é escrita apenas de um único ponto). Outro exemplo seria uma referência de matriz volátil que faz o backup de uma lista de cópia na gravação, desde que a matriz tenha sido lida apenas obtendo primeiro uma cópia local da referência a ela.

Volátil é um modificador de campo , enquanto sincronizado modifica blocos de código e methods . Assim, 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 em i1 no thread atual. Threads podem ter cópias locais de variables, e os dados não precisam ser os mesmos que os dados mantidos em outros threads. Em particular, outro thread pode ter atualizado i1 em seu thread, mas o valor no thread atual poderia ser diferente de esse valor atualizado. Na verdade, Java tem a idéia de uma memory “principal”, e essa é a memory que contém o valor “correto” atual das variables. Os threads podem ter sua própria cópia de dados para variables, e a cópia de thread pode ser diferente da memory “principal”. Então, de fato, é possível que a memory “principal” tenha um valor de 1 para i1 , para que ela tenha um valor de 2 para i1 e para que ela tenha um valor de 3 para i1 se thread1 e thread2 tiverem ambos atualizados i1, mas esse valor atualizado ainda não foi propagado para a memory “principal” ou para outros segmentos.

Por outro lado, geti2() acessa efetivamente o valor de i2 da memory “principal”. Não é permitido que uma variável volátil tenha uma cópia local de uma variável que seja diferente do valor atualmente mantido na memory “principal”. Efetivamente, uma variável declarada volátil deve ter seus dados sincronizados em todos os encadeamentos, de modo que, sempre que você acessar ou atualizar a variável em qualquer encadeamento, todos os outros encadeamentos vejam imediatamente o mesmo valor. Geralmente, as variables ​​voláteis têm um maior access e uma sobrecarga de atualização que as variables ​​”simples”. Geralmente, os encadeamentos podem ter sua própria cópia de dados para melhor eficiência.

Existem duas diferenças entre volitile e synchronized.

Em primeiro lugar sincronizado obtém e libera bloqueios em monitores que podem forçar apenas um thread de cada vez para executar um bloco de código. Esse é o aspecto bastante conhecido para sincronizar. Mas sincronizado também sincroniza a memory. Na verdade, sincronizado sincroniza toda a memory da thread com a memory “principal”. Portanto, executar o geti3() faz o seguinte:

  1. O segmento adquire o bloqueio no monitor para object isso.
  2. A memory do encadeamento libera todas as variables, ou seja, todas as suas variables ​​são efetivamente lidas da memory “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 reiniciado a partir da memory “principal”).
  4. (Quaisquer alterações nas variables ​​normalmente seriam escritas na memory “principal”, mas para geti3 () não temos alterações.)
  5. O encadeamento libera o bloqueio no monitor para o object isso.

Então, onde volatile apenas sincroniza o valor de uma variável entre memory de thread e memory “main”, sincronizada sincroniza o valor de todas as variables ​​entre memory de thread e memory “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

synchronized é o modificador de restrição de access de nível de método / nível de bloco. Ele irá certificar-se de que um thread possui o bloqueio para a seção crítica. Apenas o encadeamento, que possui um bloqueio, pode entrar no bloco synchronized . Se outros segmentos estiverem tentando acessar essa seção crítica, eles terão que esperar até que o proprietário atual libere o bloqueio.

volatile é um modificador de access variável que força todos os threads a obter o último valor da variável da memory principal. Nenhum bloqueio é necessário para acessar variables volatile . Todos os threads podem acessar o valor da variável volátil ao mesmo tempo.

Um bom exemplo para usar variável volátil: variável de Date .

Suponha que você tenha tornado a variável de data volatile . Todos os encadeamentos, que acessam essa variável, sempre obtêm os dados mais recentes da memory principal, de modo que todos os encadeamentos mostrem o valor real (real) da Data. Você não precisa de threads diferentes mostrando tempo diferente para a mesma variável. Todos os tópicos devem mostrar o valor correto da data.

insira a descrição da imagem aqui

Dê uma olhada neste artigo para entender melhor o conceito volatile .

Lawrence Dol cleary explicou sua read-write-update query .

Em relação às suas outras consultas

Quando é mais adequado declarar variables ​​voláteis do que acessá-las através de synchronization?

Você tem que usar o volatile se você acha que todos os threads devem obter o valor real da variável em tempo real, como o exemplo que expliquei para a variável Date.

É uma boa ideia usar o volátil para variables ​​que dependem de input?

A resposta será a mesma da primeira consulta.

Consulte este artigo para melhor compreensão.

  1. volatile palavra-chave volatile em java é um modificador de campo, enquanto a synchronized modifica blocos de códigos e methods.

  2. synchronized obtém e libera o bloqueio na palavra-chave java volatile do monitor não exige isso.

  3. Os encadeamentos em Java podem ser bloqueados para aguardar qualquer monitor em caso de synchronized , o que não é o caso da palavra-chave volatile em Java.

  4. synchronized método synchronized afeta o desempenho mais do que a palavra-chave volatile em Java.

  5. Já que a palavra-chave volatile em Java apenas sincroniza o valor de uma variável entre memory Thread e memory “main” enquanto a palavra-chave sincronizada sincroniza o valor de todas as variables ​​entre memory de thread e memory “principal” e bloqueia e libera um monitor para boot. Devido a esse motivo, a palavra synchronized chave synchronized em Java provavelmente terá mais sobrecarga do que volatile .

  6. Você não pode sincronizar no object nulo, mas sua variável volatile em java pode ser nula.

  7. A partir de Java 5 Escrever em um campo volatile tem o mesmo efeito de memory que o lançamento de um monitor, e a leitura de um campo volátil tem o mesmo efeito de memory que um monitor.

Eu gosto da explicação do jenkov

Visibilidade de objects compartilhados

Se dois ou mais segmentos estão compartilhando um object, sem o uso adequado de declarações voláteis ou synchronization , atualizações para o object compartilhado feitas por um segmento podem não estar visíveis para outros segmentos.

Imagine que o object compartilhado seja inicialmente armazenado na memory principal. Um encadeamento em execução na CPU um lê o object compartilhado em seu cache de CPU. Lá, faz uma alteração no object compartilhado. Contanto que o cache da CPU não tenha sido liberado de volta para a memory principal, a versão alterada do object compartilhado não estará visível para os threads em execução em outras CPUs. Desta forma, cada thread pode acabar com sua própria cópia do object compartilhado, cada cópia em um cache de CPU diferente.

O diagrama a seguir ilustra a situação do esboço. Um thread em execução na CPU esquerda copia o object compartilhado em seu cache de CPU e altera sua variável de contagem para 2. Essa alteração não é visível para outros threads em execução na CPU correta, porque a atualização para contagem não foi liberada de volta para a principal memory ainda.

insira a descrição da imagem aqui

Para resolver esse problema, você pode usar a palavra-chave volátil do Java . A palavra-chave volátil pode garantir que uma determinada variável seja lida diretamente da memory principal e sempre gravada de volta na memory principal quando atualizada.

Condições da corrida

Se dois ou mais segmentos compartilham um object e mais de um segmento atualiza variables ​​nesse object compartilhado, condições de corrida podem ocorrer.

Imagine se o thread A ler a contagem de variables ​​de um object compartilhado em seu cache de CPU. Imagine também que o segmento B faz o mesmo, mas em um cache de CPU diferente. Agora o thread A adiciona um para contar e o thread B faz o mesmo. Agora var1 foi incrementado duas vezes, uma vez em cada cache da CPU.

Se esses incrementos tivessem sido realizados sequencialmente, a contagem de variables ​​seria incrementada duas vezes e o valor original + 2 seria gravado de volta na memory principal.

No entanto, os dois incrementos foram realizados simultaneamente sem a devida synchronization. Independentemente de qual dos segmentos A e B que gravam sua versão atualizada da contagem de volta à memory principal, o valor atualizado será apenas 1 maior que o valor original, apesar dos dois incrementos.

Este diagrama ilustra uma ocorrência do problema com condições de corrida, conforme descrito acima:

insira a descrição da imagem aqui

Para resolver esse problema, você pode usar um bloco sincronizado Java . Um bloco sincronizado garante que apenas um segmento possa entrar em uma determinada seção crítica do código a qualquer momento. Blocos sincronizados também garantem que todas as variables ​​acessadas dentro do bloco sincronizado serão lidas da memory principal, e quando o encadeamento sair do bloco sincronizado, todas as variables ​​atualizadas serão retornadas para a memory principal novamente, independentemente de a variável ser declarada volátil ou não.