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 VarHandle
eu não tinha conhecimento anteriormente:
fullFence
, acquireFence
, releaseFence
, loadLoadFence
E 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:
Barreiras de memória são restrições de reordenação relacionadas às operações de leitura e gravação.
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.
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 emVarHandle
proporcionar efeitos de ordenação que são compatíveis com as suas barreiras de memória correspondentes C ++.#fullFence
é compatível comatomic_thread_fence(memory_order_seq_cst)
#acquireFence
é compatível comatomic_thread_fence(memory_order_acquire)
#releaseFence
é compatível comatomic_thread_fence(memory_order_release)
#loadLoadFence
e#storeStoreFence
nã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).
- 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:
As barreiras de memória disponíveis
VarHandle
causam algum tipo de sincronização de memória?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
VarHandle
operações voláteis#compareAndSet
estã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.
plain -> opaque -> release/acquire -> volatile (sequential consistency)
.Respostas:
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:
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
volatile
sinalizador controle os threads para parar / iniciar. Você sabe, o clássico: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?volatile
a leitura / gravação também garante consistência sequencial - que tem algumas garantias fortes e algumas pessoas desejam uma versão mais fraca disso.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
(ouVarHandle::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.
fonte
lazySet
vincular 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.volatile
palavra - 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 avolatile
palavra - chave nem sequer implica atualizações atômicas. Então, por que deveria ser chamado assim?restrict
, no entanto, lembro-me de momentos em que tive que escrever__volatile
para 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,volatile
era 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).