Suponha que eu tenha um campo acessado simultaneamente e que seja lido muitas vezes e raramente gravado.
public Object myRef = new Object();
Digamos que um Thread T1 esteja configurando myRef para outro valor, uma vez por minuto, enquanto N outros Threads lerão myRef bilhões de vezes contínua e simultaneamente. Eu só preciso que myRef seja eventualmente visível para todos os threads.
Uma solução simples seria usar um AtomicReference ou simplesmente volátil como este:
public volatile Object myRef = new Object();
No entanto, leituras voláteis após um incorrer em um custo de desempenho. Eu sei que é minúsculo, é mais algo que eu imagino do que algo que eu realmente preciso. Portanto, não vamos nos preocupar com desempenho e suponha que isso seja uma questão puramente teórica.
Portanto, a pergunta se resume a: Existe uma maneira de ignorar com segurança leituras voláteis de referências que raramente são gravadas, fazendo algo no site de gravação?
Após algumas leituras, parece que as barreiras de memória podem ser o que eu preciso. Portanto, se um construto como esse existisse, meu problema seria resolvido:
- Escreva
- Invocar barreira (sincronização)
- Tudo é sincronizado e todos os threads verão o novo valor. (sem um custo permanente nos sites de leitura, pode ser obsoleto ou incorrer em um custo único à medida que os caches são sincronizados, mas depois disso tudo volta ao campo normal até a próxima gravação).
Existe tal construção em Java ou em geral? Neste ponto, não posso deixar de pensar que, se algo assim existisse, ele já teria sido incorporado aos pacotes atômicos pelas pessoas mais inteligentes que os mantinham. (A leitura e a gravação desproporcionalmente frequentes podem não ter sido um caso para cuidar?) Então, talvez haja algo errado em meu pensamento e essa construção não seja possível?
Eu já vi alguns exemplos de código usar 'volátil' para uma finalidade semelhante, explorando o que acontece antes do contrato. Há um campo de sincronização separado, por exemplo:
public Object myRef = new Object();
public volatile int sync = 0;
e na escrita do tópico / site:
myRef = new Object();
sync += 1 //volatile write to emulate barrier
Não sei se isso funciona, e alguns argumentam que isso funciona apenas na arquitetura x86. Depois de ler as seções relacionadas no JMS, acho que é garantido que funcione apenas se essa gravação volátil estiver associada a uma leitura volátil dos threads que precisam ver o novo valor de myRef. (Portanto, não se livre da leitura volátil).
Voltando à minha pergunta original; Isso é possível em tudo? É possível em Java? É possível em uma das novas APIs no Java 9 VarHandles?
fonte
sync += 1;
e o encadeamento do leitor lerem osync
valor, eles também verão amyRef
atualização. Como você só precisa que os leitores vejam a atualização eventualmente , poderá usar isso como vantagem para ler a sincronização apenas a cada milésima iteração do encadeamento do leitor, ou algo semelhante. Mas você pode fazer um truque semelhante comvolatile
, demasiado - apenas cache omyRef
campo nos leitores para 1000 iterações, em seguida, lê-lo novamente usando volátil ...sync
campo, não, os leitores não tocariam nosync
campo em todas as iterações, eles fariam isso oportunisticamente, quando desejassem verificar se houve uma atualização. Dito isto, uma solução mais simples seria para armazenar em cache omyRef
para um 1000 rounds, então relê-lo ...Respostas:
Então, basicamente, você quer a semântica de a
volatile
sem o custo de tempo de execução.Eu não acho que isso é possível.
O problema é que o custo de tempo de execução
volatile
é devido às instruções que implementam as barreiras de memória no gravador e no código do leitor. Se você "otimizar" o leitor, livrando-se de sua barreira de memória, não terá mais garantia de que o leitor verá o novo valor "raramente escrito" quando ele for realmente escrito.FWIW, algumas versões da
sun.misc.Unsafe
classe fornecem explícitaloadFence
,storeFence
efullFence
métodos mas não acho que usá-los trará nenhum benefício de desempenho sobre o uso de avolatile
.Hipoteticamente ...
o que você deseja é que um processador em um sistema com vários processadores seja capaz de informar todos os outros processadores:
Infelizmente, os ISAs modernos não suportam isso.
Na prática, cada processador controla seu próprio cache.
fonte
Não tenho certeza se isso está correto, mas posso resolver isso usando uma fila.
Crie uma classe que agrupe um atributo ArrayBlockingQueue. A classe possui um método de atualização e um método de leitura. O método de atualização lança o novo valor na fila e remove todos os valores, exceto o último valor. O método read retorna o resultado de uma operação de espiada na fila, isto é, lê, mas não remove. Os encadeamentos que espreitam o elemento na frente da fila fazem isso sem impedimentos. Os encadeamentos que atualizam a fila fazem isso de forma limpa.
fonte
ReentrantReadWriteLock
que é projetado para poucas gravações e muitas leituras.Você pode usar o
StampedLock
que é projetado para o mesmo caso de poucas gravações e muitas leituras, mas também é possível tentar otimizações. Exemplo:Torne seu estado imutável e permita modificações controladas apenas pela clonagem do objeto existente e alterando apenas as propriedades necessárias por meio do construtor. Depois que o novo estado é construído, você o atribui à referência que está sendo lida pelos vários threads de leitura. Dessa forma, as threads de leitura incorrem em custo zero .
fonte
X86 fornece TSO; você obtém cercas [LoadLoad] [LoadStore] [StoreStore] gratuitamente.
Uma leitura volátil requer semântica de liberação.
E como você pode ver, isso já é fornecido pelo X86 gratuitamente.
No seu caso, a maioria das chamadas é de leitura e o cacheline já estará no cache local.
Há um preço a pagar nas otimizações no nível do compilador, mas no nível do hardware, uma leitura volátil é tão cara quanto uma leitura regular.
Por outro lado, a gravação volátil é mais cara porque requer um [StoreLoad] para garantir consistência sequencial (na JVM, isso é feito usando um
lock addl %(rsp),0
ou um MFENCE). Como as gravações raramente ocorrem na sua situação, isso não é um problema.Eu teria cuidado com as otimizações nesse nível, porque é muito fácil tornar o código mais complexo do que o necessário. É melhor orientar seus esforços de desenvolvimento por alguns benchmarks, por exemplo, usando JMH e, de preferência, testá-lo em hardware real. Também poderia haver outras criaturas desagradáveis escondidas como compartilhamento falso.
fonte