As variáveis ​​estáticas são compartilhadas entre os threads?

95

Meu professor em uma classe de Java de nível superior sobre threading disse algo que eu não tinha certeza.

Ele afirmou que o código a seguir não atualizaria necessariamente a readyvariável. Segundo ele, os dois threads não necessariamente compartilham a variável estática, especificamente no caso em que cada thread (thread principal versus ReaderThread) está rodando em seu próprio processador e, portanto, não compartilham os mesmos registradores / cache / etc e uma CPU não atualizará o outro.

Essencialmente, ele disse que é possível que readyseja atualizado no thread principal, mas NÃO no ReaderThread, de modo que ReaderThreadfará um loop infinito.

Ele também afirmou que era possível para o programa imprimir 0ou 42. Eu entendo como 42poderia ser impresso, mas não 0. Ele mencionou que esse seria o caso quando a numbervariável fosse definida com o valor padrão.

Pensei que talvez não fosse garantido que a variável estática seja atualizada entre os threads, mas isso me parece muito estranho para Java. Tornar readyvolátil corrige este problema?

Ele mostrou este código:

public class NoVisibility {  
    private static boolean ready;  
    private static int number;  
    private static class ReaderThread extends Thread {   
        public void run() {  
            while (!ready)   Thread.yield();  
            System.out.println(number);  
        }  
    }  
    public static void main(String[] args) {  
        new ReaderThread().start();  
        number = 42;  
        ready = true;  
    }  
}
dontocsata
fonte
A visibilidade de variáveis ​​não locais não depende se são variáveis ​​estáticas, campos de objeto ou elementos de matriz, todos eles têm as mesmas considerações. (Com o problema de que os elementos da matriz não podem ser tornados voláteis.)
Paŭlo Ebermann
1
pergunte ao seu professor que tipo de arquitetura ele acha que seria possível ver '0'. No entanto, em teoria, ele está certo.
bestsss
4
@bestsss Fazer esse tipo de pergunta revelaria ao professor que ele não entendeu o que estava dizendo. A questão é que os programadores competentes entendem o que é garantido e o que não é e não confiam em coisas que não são garantidas, pelo menos não sem entender precisamente o que não é garantido e por quê.
David Schwartz
Eles são compartilhados entre tudo carregado pelo mesmo carregador de classe. Incluindo tópicos.
Marquês de Lorne
Seu professor (e a resposta aceita) estão 100% corretos, mas vou mencionar que isso raramente acontece - esse é o tipo de problema que vai se esconder por anos e só se manifestar quando for mais prejudicial. Mesmo os testes curtos que tentam expor o problema tendem a agir como se tudo estivesse bem (provavelmente porque eles não têm tempo para que a JVM faça muita otimização), portanto, é realmente uma boa questão estar ciente.
Bill K de

Respostas:

75

Não há nada de especial sobre variáveis ​​estáticas quando se trata de visibilidade. Se eles estiverem acessíveis, qualquer thread pode chegar até eles, então é mais provável que você veja problemas de simultaneidade porque eles estão mais expostos.

Há um problema de visibilidade imposto pelo modelo de memória da JVM. Aqui está um artigo falando sobre o modelo de memória e como as gravações se tornam visíveis para os threads . Você não pode contar com as mudanças que um encadeamento torna visível para outros encadeamentos em tempo hábil (na verdade, a JVM não tem obrigação de tornar essas alterações visíveis para você, em qualquer período de tempo), a menos que você estabeleça um relacionamento acontece antes .

Aqui está uma citação desse link (fornecida no comentário de Jed Wesley-Smith):

O Capítulo 17 da Especificação da linguagem Java define a relação acontece antes nas operações de memória, como leituras e gravações de variáveis ​​compartilhadas. Os resultados de uma gravação por um encadeamento são garantidos como visíveis para uma leitura por outro encadeamento somente se a operação de gravação acontecer - antes da operação de leitura. As construções sincronizadas e voláteis, bem como os métodos Thread.start () e Thread.join (), podem formar relacionamentos acontece antes. Em particular:

  • Cada ação em um encadeamento acontece - antes de cada ação nesse encadeamento que vem posteriormente na ordem do programa.

  • Um desbloqueio (bloco sincronizado ou saída de método) de um monitor ocorre - antes de cada bloqueio subsequente (bloco sincronizado ou entrada de método) desse mesmo monitor. E como a relação acontece-antes é transitiva, todas as ações de um encadeamento antes do desbloqueio acontecem - antes de todas as ações subsequentes a qualquer encadeamento bloqueando esse monitor.

  • Uma gravação em um campo volátil acontece - antes de cada leitura subsequente desse mesmo campo. As gravações e leituras de campos voláteis têm efeitos de consistência de memória semelhantes aos de entrada e saída de monitores, mas não envolvem bloqueio de exclusão mútua.

  • Uma chamada para iniciar em um segmento acontece - antes de qualquer ação no segmento iniciado.

  • Todas as ações em um encadeamento acontecem - antes que qualquer outro encadeamento retorne com sucesso de uma junção nesse encadeamento.

Nathan Hughes
fonte
3
Na prática, "em tempo hábil" e "sempre" são sinônimos. Seria muito possível que o código acima nunca terminasse.
TREE
4
Além disso, isso demonstra outro anti-padrão. Não use o volátil para proteger mais de uma parte do estado compartilhado. Aqui, number e ready são duas partes do estado, e para atualizar / ler ambos consistentemente, você precisa da sincronização real.
TREE
5
A parte sobre eventualmente se tornar visível está errada. Sem qualquer explícita acontece-antes de relacionamento não há garantia de que qualquer gravação irá sempre ser visto por outro segmento, como o JIT é bem dentro de seus direitos para impingir a leitura em um registo e, em seguida, você nunca verá qualquer atualização. Qualquer eventual carregamento é sorte e não deve ser invocado.
Jed Wesley-Smith
2
"a menos que você use a palavra-chave volátil ou sincronize." deve ler "a menos que haja uma relação acontece-antes relevante entre o escritor e o leitor" e este link: download.oracle.com/javase/6/docs/api/java/util/concurrent/…
Jed Wesley-Smith
2
@bestsss bem localizado. Infelizmente, o ThreadGroup está corrompido de várias maneiras.
Jed Wesley-Smith
37

Ele estava falando sobre visibilidade e não deve ser interpretado tão literalmente.

Variáveis ​​estáticas são de fato compartilhadas entre threads, mas as mudanças feitas em uma thread podem não ser visíveis para outra thread imediatamente, fazendo parecer que há duas cópias da variável.

Este artigo apresenta uma visão consistente com a forma como ele apresentou as informações:

Primeiro, você precisa entender um pouco sobre o modelo de memória Java. Eu me esforcei um pouco ao longo dos anos para explicá-lo bem e resumidamente. A partir de hoje, a melhor maneira que posso pensar para descrevê-lo é se você imaginar desta forma:

  • Cada thread em Java ocorre em um espaço de memória separado (isso é claramente falso, então tenha paciência comigo).

  • Você precisa usar mecanismos especiais para garantir que a comunicação aconteça entre esses threads, como faria em um sistema de passagem de mensagens.

  • As gravações de memória que acontecem em um thread podem "vazar" e ser vistas por outro thread, mas isso não é garantido de forma alguma. Sem comunicação explícita, você não pode garantir quais gravações são vistas por outros threads, ou mesmo a ordem em que são vistas.

...

modelo de fio

Mas, novamente, este é simplesmente um modelo mental para pensar sobre encadeamento e volátil, não literalmente como a JVM funciona.

Bert F
fonte
12

Basicamente, é verdade, mas na verdade o problema é mais complexo. A visibilidade dos dados compartilhados pode ser afetada não apenas pelos caches da CPU, mas também pela execução fora de ordem das instruções.

Portanto, Java define um Modelo de Memória , que indica sob quais circunstâncias os encadeamentos podem ver o estado consistente dos dados compartilhados.

No seu caso particular, adicionar volatilegarante visibilidade.

axtavt
fonte
8

Eles são "compartilhados", é claro, no sentido de que ambos se referem à mesma variável, mas não necessariamente veem as atualizações um do outro. Isso é verdadeiro para qualquer variável, não apenas estática.

E, em teoria, as gravações feitas por outro encadeamento podem parecer estar em uma ordem diferente, a menos que as variáveis ​​sejam declaradas volatileou as gravações sejam explicitamente sincronizadas.

biziclop
fonte
4

Em um único carregador de classe, os campos estáticos são sempre compartilhados. Para definir o escopo de dados explicitamente para threads, você deseja usar um recurso como ThreadLocal.

Kirk Woll
fonte
2

Quando você inicializa a variável de tipo primitivo estático, o padrão java atribui um valor para variáveis ​​estáticas

public static int i ;

quando você define a variável desta forma, o valor padrão de i = 0; é por isso que existe a possibilidade de obter 0. então o thread principal atualiza o valor booleano pronto para verdadeiro. uma vez que ready é uma variável estática, o thread principal e o outro thread fazem referência ao mesmo endereço de memória, então a variável ready muda. assim, o thread secundário sai do loop while e imprime o valor. ao imprimir o valor inicializado, o valor de número é 0. se o processo de thread passou por um loop while antes da variável de número de atualização de thread principal. então existe a possibilidade de imprimir 0

NuOne
fonte
-2

@dontocsata você pode voltar para seu professor e ensiná-lo um pouco :)

poucas notas do mundo real e independentemente do que você vê ou ouve. OBSERVE que as palavras abaixo referem-se a este caso particular na ordem exata mostrada.

As 2 variáveis ​​a seguir residirão na mesma linha de cache em praticamente qualquer arquitetura conhecida.

private static boolean ready;  
private static int number;  

Thread.exit (thread principal) tem garantia de saída e exit causar um limite de memória, devido à remoção do encadeamento do grupo de encadeamentos (e muitos outros problemas). (é uma chamada sincronizada e não vejo uma maneira única de ser implementada sem a parte de sincronização, já que o ThreadGroup também deve ser encerrado se nenhum encadeamento do daemon for deixado, etc).

O encadeamento iniciado ReaderThreadmanterá o processo ativo, pois não é um daemon! Portanto, readye numberserá liberado junto (ou o número anterior, se ocorrer uma troca de contexto) e não há razão real para reordenar neste caso, pelo menos eu não consigo nem pensar em um. Você vai precisar de algo realmente estranho para ver qualquer coisa, mas42 . Mais uma vez, presumo que ambas as variáveis ​​estáticas estarão na mesma linha de cache. Eu simplesmente não consigo imaginar uma linha de cache de 4 bytes OU uma JVM que não os atribuirá em uma área contínua (linha de cache).

bestsss
fonte
3
@bestsss embora tudo isso seja verdade hoje, ele depende da implementação atual da JVM e da arquitetura de hardware para sua verdade, não na semântica do programa. Isso significa que o programa ainda está quebrado, embora possa funcionar. É fácil encontrar uma variante trivial deste exemplo que realmente falha nas formas especificadas.
Jed Wesley-Smith
1
Eu disse que não segue as especificações, no entanto, como um professor, pelo menos, encontra um exemplo adequado que pode realmente falhar em alguma arquitetura de commodity, então o exemplo é meio real.
bestsss
6
Possivelmente o pior conselho sobre como escrever código thread-safe que já vi.
Lawrence Dol
4
@Bestsss: A resposta simples para sua pergunta é simplesmente esta: "Codifique para a especificação e documento, não para os efeitos colaterais de seu sistema ou implementação em particular". Isso é especialmente importante em uma plataforma de máquina virtual projetada para ser independente do hardware subjacente.
Lawrence Dol
1
@Bestsss: O professor aponta que (a) o código pode funcionar bem quando você o testa, e (b) o código está quebrado porque depende da operação do hardware e não das garantias da especificação. A questão é que parece que está tudo bem, mas não está.
Lawrence Dol