Para que servem as cercas de memória em Java?

18

Enquanto tentava entender como SubmissionPublisher( código-fonte no Java SE 10, OpenJDK | docs ), uma nova classe adicionada ao Java SE na versão 9, foi implementada, deparei-me com algumas chamadas de API para as quais VarHandleeu não tinha conhecimento anteriormente:

fullFence, acquireFence, releaseFence, loadLoadFenceE storeStoreFence.

Depois de fazer algumas pesquisas, especialmente com relação ao conceito de barreiras / cercas de memória (eu as ouvi anteriormente, sim; mas nunca as usei, portanto, não estava familiarizado com sua semântica), acho que tenho um entendimento básico sobre o que elas servem. . No entanto, como minhas perguntas podem surgir de um equívoco, quero garantir que eu acertei em primeiro lugar:

  1. Barreiras de memória são restrições de reordenação relacionadas às operações de leitura e gravação.

  2. As barreiras de memória podem ser categorizadas em duas categorias principais: barreiras de memória unidirecionais e bidirecionais, dependendo se elas definem restrições nas leituras, gravações ou ambas.

  3. O C ++ suporta uma variedade de barreiras de memória , no entanto, elas não coincidem com as fornecidas por VarHandle. No entanto, algumas das barreiras de memória disponíveis em VarHandleproporcionar efeitos de ordenação que são compatíveis com as suas barreiras de memória correspondentes C ++.

    • #fullFence é compatível com atomic_thread_fence(memory_order_seq_cst)
    • #acquireFence é compatível com atomic_thread_fence(memory_order_acquire)
    • #releaseFence é compatível com atomic_thread_fence(memory_order_release)
    • #loadLoadFencee #storeStoreFencenão possui contraparte C ++ compatível

A palavra compatível parece ser realmente importante aqui, pois a semântica difere claramente quando se trata de detalhes. Por exemplo, todas as barreiras de C ++ são bidirecionais, enquanto as barreiras de Java não são (necessariamente).

  1. A maioria das barreiras de memória também tem efeitos de sincronização. Esses dependem especialmente do tipo de barreira usado e das instruções de barreira executadas anteriormente em outros segmentos. Como as implicações completas de uma instrução de barreira são específicas do hardware, continuarei com as barreiras de nível superior (C ++). Em C ++, por exemplo, as alterações feitas antes de uma instrução de barreira de liberação são visíveis para um thread que executa uma instrução de barreira de aquisição .

Minhas suposições estão corretas? Nesse caso, minhas perguntas resultantes são:

  1. As barreiras de memória disponíveis VarHandlecausam algum tipo de sincronização de memória?

  2. Independentemente de causar ou não sincronização de memória, para que podem ser úteis restrições de reordenamento em Java? O Java Memory Model já oferece algumas garantias muito fortes em relação à solicitação quando campos, bloqueios ou VarHandleoperações voláteis #compareAndSetestão envolvidos.

Caso você esteja procurando um exemplo: A mencionada acima BufferedSubscription, uma classe interna de SubmissionPublisher(fonte vinculada acima), estabeleceu uma barreira completa na linha 1079 (função growAndAdd; como o site vinculado não suporta identificadores de fragmentos, apenas CTRL + F para isso) ) No entanto, não está claro para mim o que existe.

Quaffel
fonte
11
Tentei responder, mas, para simplificar, elas existem porque as pessoas querem um modo mais fraco do que o Java. Em ordem crescente, estes seriam: plain -> opaque -> release/acquire -> volatile (sequential consistency).
Eugene

Respostas:

11

Isso é principalmente uma não resposta, realmente (inicialmente queria fazer um comentário, mas como você pode ver, é muito tempo). Só que eu me questionei bastante, fiz muita leitura e pesquisa e, neste momento, posso dizer com segurança: isso é complicado. Até escrevi vários testes com o jcstress para descobrir como eles realmente funcionam (enquanto observamos o código de montagem gerado) e, embora alguns deles de alguma forma fizessem sentido, o assunto em geral não é nada fácil.

A primeira coisa que você precisa entender:

O Java Language Specification (JLS) não menciona barreiras em nenhum lugar. Isso, para java, seria um detalhe de implementação: ele realmente age em termos de acontece antes da semântica. Para poder especificá-los adequadamente de acordo com o JMM (Java Memory Model), o JMM precisaria mudar bastante .

Esse trabalho está em andamento.

Segundo, se você realmente deseja arranhar a superfície aqui, esta é a primeira coisa a se observar . A conversa é incrível. Minha parte favorita é quando Herb Sutter levanta os cinco dedos e diz: "É quantas pessoas podem realmente e corretamente trabalhar com elas". Isso deve lhe dar uma dica da complexidade envolvida. No entanto, existem alguns exemplos triviais fáceis de entender (como um contador atualizado por vários encadeamentos que não se preocupam com outras garantias de memória, mas apenas se preocupam com o fato de serem incrementados corretamente).

Outro exemplo é quando (em java) você deseja que um volatilesinalizador controle os threads para parar / iniciar. Você sabe, o clássico:

volatile boolean stop = false; // on thread writes, one thread reads this    

Se você trabalha com java, você saberia que sem volatile esse código está quebrado (você pode ler por que o bloqueio de verificação dupla está quebrado sem ele, por exemplo). Mas você também sabe que para algumas pessoas que escrevem código de alto desempenho isso é demais? volatilea leitura / gravação também garante consistência sequencial - que tem algumas garantias fortes e algumas pessoas desejam uma versão mais fraca disso.

Um sinalizador de segmento seguro, mas não volátil? Sim, exatamente: VarHandle::set/getOpaque.

E você pergunta por que alguém pode precisar disso, por exemplo? Nem todo mundo está interessado em todas as mudanças suportadas por um volatile.

Vamos ver como vamos conseguir isso em java. Primeiro de tudo, esses exóticos coisas já existia na API: AtomicInteger::lazySet. Isso não é especificado no Java Memory Model e não possui uma definição clara ; ainda as pessoas usavam (LMAX, afaik ou isso para mais leitura ). IMHO, AtomicInteger::lazySeté VarHandle::releaseFence(ou VarHandle::storeStoreFence).


Vamos tentar responder por que alguém precisa disso ?

O JMM tem basicamente duas maneiras de acessar um campo: simples e volátil (que garante consistência sequencial ). Todos esses métodos que você mencionou estão lá para trazer algo entre esses dois - liberar / adquirir semântica ; acho que há casos em que as pessoas realmente precisam disso.

Um relaxamento ainda maior da liberação / aquisição seria opaco , o que ainda estou tentando entender completamente .


Assim, o resultado final (seu entendimento é bastante correto, btw): se você planeja usar isso em java - eles não têm especificação no momento, faça-o por sua conta e risco. Se você deseja entendê-los, os modos equivalentes a C ++ são o ponto de partida.

Eugene
fonte
11
Não tente descobrir o significado de lazySetvincular a respostas antigas, a documentação atual diz precisamente o que significa hoje em dia. Além disso, é enganoso dizer que o JMM tem apenas dois modos de acesso. Temos leitura e gravação voláteis , que juntas podem estabelecer um relacionamento de antes do acontecimento.
Holger
11
Eu estava escrevendo algo mais sobre isso. Considere que cas é ao mesmo tempo uma leitura e uma gravação, agindo como uma barreira total, e você pode entender por que relaxar é desejado. Por exemplo, ao implementar um bloqueio, a primeira ação é cas (0, 1) na contagem de bloqueios, mas você só precisa adquirir semântica (como leitura volátil), enquanto a gravação final de 0 para desbloquear deve ter liberação semântica (como gravação volátil) ), para que ocorra um problema antes entre o desbloqueio e o bloqueio subsequente. A aquisição / liberação é ainda mais fraca que a leitura / gravação volátil em relação a threads usando bloqueios diferentes.
Holger
11
@ Peter Cordes: A primeira versão em C com uma volatilepalavra - chave foi C99, cinco anos após o Java, mas ainda faltava semântica útil, mesmo o C ++ 03 não possui modelo de memória. As coisas que C ++ chama de "atômica" também são muito mais jovens que Java. E a volatilepalavra - chave nem sequer implica atualizações atômicas. Então, por que deveria ser chamado assim?
Holger
11
@ PeterCordes, talvez, eu esteja confundindo isso restrict, no entanto, lembro-me de momentos em que tive que escrever __volatilepara usar uma extensão de compilador que não fosse de palavra-chave. Então, talvez, ele não implementou completamente o C89? Não me diga que sou tão velha. Antes do Java 5, volatileera muito mais próximo de C. Mas o Java não tinha MMIO, então seu objetivo sempre era o multi-threading, mas a semântica pré-Java 5 não era muito útil para isso. Portanto, o lançamento / aquisição como semântica foi adicionado, mas ainda assim, não é atômico (as atualizações atômicas são um recurso adicional construído sobre ele).
Holger
2
@ Eugene em relação a isso , meu exemplo foi específico para o uso de cas para travamento que seria adquirido. Uma trava de contagem regressiva suportaria decréscimos atômicos com liberação semântica, seguidos pelo segmento atingir zero, inserindo uma cerca de aquisição e executando a ação final. Obviamente, existem outros casos de atualizações atômicas em que a cerca completa permanece necessária.
Holger