Acontece antes de relacionamentos com campos voláteis e blocos sincronizados em Java – e seu impacto em variables ​​não voláteis?

Ainda sou muito novo no conceito de threading e tento entender mais sobre isso. Recentemente, me deparei com um post no blog What Volatile Means in Java, de Jeremy Manson, onde ele escreve:

Quando um thread grava em uma variável volátil, e outro thread vê esse write, o primeiro thread informa ao segundo sobre todo o conteúdo da memory até executar a gravação para aquela variável volátil. […] todo o conteúdo da memory visto pelo Thread 1, antes de escrever para [volatile] ready , deve estar visível para o Thread 2, depois de ler o valor true para ready . [ênfase adicionada por mim]

Agora, isso significa que todas as variables ​​(voláteis ou não) mantidas na memory do Thread 1 no momento da gravação na variável volátil se tornarão visíveis para o Thread 2 depois de ler essa variável volátil? Em caso afirmativo, é possível confundir essa instrução junto das origens oficiais de documentação / Oracle do Java? E de qual versão do Java em diante isso funcionará?

Em particular, se todos os Threads compartilharem as seguintes variables ​​de class:

 private String s = "running"; private volatile boolean b = false; 

E o segmento 1 executa o seguinte primeiro:

 s = "done"; b = true; 

E o Thread 2 então executa depois (depois que o Thread 1 escreveu no campo volátil):

 boolean flag = b; //read from volatile System.out.println(s); 

Isso seria garantido para imprimir “feito”?

O que aconteceria se, em vez de declarar b como volatile eu colocasse a gravação e a leitura em um bloco synchronized ?

Além disso, em uma discussão intitulada ” As variables ​​estáticas são compartilhadas entre os threads? “, @TREE escreve :

Não use volátil para proteger mais de uma parte do estado compartilhado.

Por quê? (Desculpe, eu não posso comentar ainda sobre outras questões, ou eu teria perguntado lá …)

Sim, é garantido que o thread 2 será impresso “concluído”. Claro, isto é, se a gravação para b no Tópico 1 realmente acontecer antes da leitura de b no Tópico 2, em vez de acontecer ao mesmo tempo, ou antes!

O coração do raciocínio aqui é o relacionamento acontece antes . Execuções de programas multithread são vistas como sendo feitas de events. Os events podem ser relacionados por relações antes dos relacionamentos, que dizem que um evento acontece antes do outro. Mesmo que dois events não estejam diretamente relacionados, se você puder rastrear uma cadeia de ocorrências – antes de relacionamentos de um evento para outro, então você pode dizer que um acontece antes do outro.

No seu caso, você tem os seguintes events:

  • Thread 1 escreve para s
  • Thread 1 escreve para b
  • O fio 2 lê de b
  • O fio 2 lê de s

E as seguintes regras entram em jogo:

  • “Se x e y são ações do mesmo encadeamento e x vem antes de y na ordem do programa, então hb (x, y).” (a regra de ordem de programa )
  • “Uma gravação em um campo volátil (§8.3.1.4) acontece – antes de cada leitura subseqüente desse campo.” (a regra volátil )

O seguinte acontece – antes que existam relacionamentos:

  • Thread 1 escreve para s acontece antes de Thread 1 escreve para b (regra de ordem de programa)
  • Thread 1 escreve para b acontece antes de Thread 2 lê de b (regra volátil)
  • O segmento 2 lê de b acontece antes de o segmento 2 ler de s (regra de ordem de programa)

Se você seguir essa cadeia, poderá ver isso como resultado:

  • Thread 1 escreve para s acontece antes de Thread 2 lê de s

O que aconteceria se, em vez de declarar b como volátil, eu colocasse a gravação e a leitura em um bloco sincronizado?

Se e somente se você proteger todos esses blocos sincronizados com o mesmo bloqueio, você terá a mesma garantia de visibilidade que com seu exemplo volatile . Além disso, você terá exclusão mútua da execução de tais blocos sincronizados.

Não use volátil para proteger mais de uma parte do estado compartilhado.

Por quê?

volatile não garante atomicidade: no seu exemplo, a variável s também pode ter sido mutada por outros threads após a gravação que você está mostrando; o thread de leitura não terá qualquer garantia sobre qual valor ele vê. A mesma coisa vale para as gravações que ocorrem depois da leitura do volatile , mas antes da leitura de s .

O que é seguro fazer, e feito na prática, é compartilhar um estado imutável transitivamente acessível a partir da referência escrita a uma variável volatile . Então talvez esse seja o significado pretendido por “um pedaço de estado compartilhado”.

é possível confundir essa declaração junto das fonts oficiais de documentação / Oracle do Java?

Citações da especificação:

17.4.4. Ordem de Sincronização

Uma gravação para uma variável volátil v (§8.3.1.4) sincroniza-se com todas as leituras subsequentes de v por qualquer thread (onde “subsequente” é definido de acordo com a ordem de synchronization).

17.4.5. Acontece antes do pedido

Se x e y são ações do mesmo encadeamento e x vem antes de y na ordem do programa, então hb (x, y).

Se uma ação x sincroniza com a seguinte ação y, então também temos hb (x, y).

Isso deve ser o suficiente.

E de qual versão do Java em diante isso funcionará?

Java Language Specification, 3rd Edition introduziu a reescrita da especificação do Modelo de Memória, que é a chave para as garantias acima. NB a maioria das versões anteriores agia como se as garantias estivessem lá e muitas linhas de código realmente dependessem disso. As pessoas ficaram surpresas quando descobriram que as garantias de fato não estavam lá.

Isso seria garantido para imprimir “feito”?

Como dito em Java Concurrency in Practice :

Quando o thread A grava em uma variável volatile e, subsequentemente, o thread B lê a mesma variável, os valores de todas as variables ​​que estavam visíveis para A antes de gravar na variável volatile tornam-se visíveis para B após a leitura da variável volatile .

Então, sim , isso garante a impressão “feito”.

O que aconteceria se, em vez de declarar b como volátil, eu colocasse a gravação e a leitura em um bloco sincronizado?

Isso também garantirá o mesmo.

Não use volátil para proteger mais de uma parte do estado compartilhado.

Por quê?

Porque, volátil garante apenas a visibilidade. Não garante atomicidade. Se temos duas gravações voláteis em um método que está sendo acessado por um thread A e outro thread B está acessando essas variables ​​voláteis, então, enquanto o thread A está executando o método, pode ser possível que o thread A seja precedido pelo thread B no meio de operações (por exemplo, após a primeira gravação volátil, mas antes da segunda gravação volátil pelo encadeamento A ). Então, para garantir a atomicidade da operação, a synchronization é a saída mais viável.