As variáveis em Java são divididas em duas categorias: variáveis locais e variáveis de classe. Variáveis locais referem-se a variáveis definidas em um método, como variáveis definidas no método run. Para essas variáveis, não há problema de compartilhamento entre threads. Portanto, eles não requerem sincronização de dados. Variáveis de classe são variáveis definidas em uma classe e seu escopo é toda a classe. Essas variáveis podem ser compartilhadas por vários threads. Portanto, precisamos realizar a sincronização de dados nessas variáveis.
A sincronização de dados significa que apenas um thread pode acessar variáveis de classe sincronizadas ao mesmo tempo. Depois que o thread atual tiver acessado essas variáveis, outros threads poderão continuar a acessá-las. O acesso mencionado aqui refere-se ao acesso com operações de gravação. Se todos os threads que acessam variáveis de classe forem operações de leitura, a sincronização de dados geralmente não será necessária. Então, o que acontece se a sincronização de dados não for executada em variáveis de classe compartilhadas? Vamos primeiro ver o que acontece com o seguinte código:
Copie o código do código da seguinte forma:
teste de pacote;
classe pública MyThread estende Thread
{
público estático int n = 0;
execução de vazio público ()
{
interno m = n;
colheita();
m++;
n=m;
}
public static void main(String[] args) lança exceção
{
MeuThread meuThread = new MeuThread();
Tópico tópicos[] = new Tópico[100];
for (int i = 0; i < threads.length; i++)
threads[i] = new Thread(meuThread);
for (int i = 0; i < threads.length; i++)
tópicos[i].start();
for (int i = 0; i < threads.length; i++)
threads[i].join();
System.out.println("n = " + MyThread.n);
}
}
Os possíveis resultados da execução do código acima são os seguintes:
Copie o código do código da seguinte forma:
n=59
Muitos leitores podem ficar surpresos ao ver esse resultado. Este programa obviamente inicia 100 threads e então cada thread incrementa a variável estática n em 1. Por fim, use o método join para executar todos os 100 threads e, em seguida, produza o valor n. Normalmente, o resultado deve ser n = 100. Mas o resultado é inferior a 100.
Na verdade, o culpado deste resultado são os “dados sujos” que frequentemente mencionamos. A instrução yield() no método run é o iniciador de "dados sujos" (sem adicionar uma instrução de rendimento, "dados sujos" também podem ser gerados, mas não será tão óbvio. Somente alterando 100 para um número maior será muitas vezes ocorre. Gerar "dados sujos", chamar rendimento neste exemplo é amplificar o efeito de "dados sujos"). A função do método yield é pausar o thread, ou seja, fazer com que o thread que chama o método yield desista temporariamente dos recursos da CPU, dando à CPU a chance de executar outros threads. Para ilustrar como este programa gera "dados sujos", vamos supor que apenas dois threads sejam criados: thread1 e thread2. Como o método start do thread1 é chamado primeiro, o método run do thread1 geralmente será executado primeiro. Quando o método run de thread1 é executado na primeira linha (int m = n;), o valor de n é atribuído a m. Quando o método de rendimento da segunda linha é executado, o thread1 parará temporariamente de executar. Quando o thread1 for pausado, o thread2 começará a ser executado após obter os recursos da CPU (o thread2 estava no estado pronto antes). m = n;), como n ainda é 0 quando o thread1 é executado para produzir, o valor obtido por m no thread2 também é 0. Isso faz com que os valores m de thread1 e thread2 obtenham 0. Depois de executarem o método de rendimento, todos começam em 0 e adicionam 1. Portanto, não importa quem o executa primeiro, o valor final de n é 1, mas a este n é atribuído um valor por thread1 e thread2 respectivamente. Alguém pode perguntar, se houver apenas n++, serão gerados “dados sujos”? A resposta é sim. Então n++ é apenas uma instrução, então como entregar a CPU para outros threads durante a execução? Na verdade, este é apenas um fenômeno superficial. Depois que n++ é compilado em uma linguagem intermediária (também chamada de bytecode) pelo compilador Java, ele não é uma linguagem. Vamos ver em qual linguagem intermediária Java o código Java a seguir será compilado.
Copie o código do código da seguinte forma:
execução de vazio público ()
{
n++;
}
Código de linguagem intermediária compilado
Copie o código do código da seguinte forma:
execução de vazio público ()
{
aload_0
idiota
obter campo
íconest_1
eu adicionei
colocar campo
retornar
}
Você pode ver que há apenas uma instrução n++ no método run, mas após a compilação, existem 7 instruções de linguagem intermediária. Não precisamos saber quais são as funções dessas declarações, basta olhar as declarações nas linhas 005, 007 e 008. A linha 005 é getfield. De acordo com seu significado em inglês, sabemos que queremos obter um determinado valor. Como há apenas um n aqui, não há dúvida de que queremos obter o valor de n. Não é difícil adivinhar que o iadd na linha 007 é adicionar 1 ao valor n obtido. Acho que você deve ter adivinhado o significado de putfield na linha 008. Ele é responsável por atualizar n após adicionar 1 de volta à variável de classe n. Falando nisso, você ainda pode ter uma dúvida. Ao executar n++, basta apenas somar n por 1. Por que dá tanto trabalho? Na verdade, isso envolve um problema de modelo de memória Java.
O modelo de memória Java é dividido em área de armazenamento principal e área de armazenamento de trabalho. A área de armazenamento principal armazena todas as instâncias em Java. Ou seja, depois de usarmos new para criar um objeto, o objeto e seus métodos internos, variáveis, etc. são salvos nesta área, e n na classe MyThread é salvo nesta área. O armazenamento principal pode ser compartilhado por todos os threads. A área de armazenamento de trabalho é a pilha de threads de que falamos anteriormente. Nesta área são armazenadas as variáveis definidas no método run e o método chamado pelo método run, ou seja, variáveis do método. Quando um thread deseja modificar as variáveis na área de armazenamento principal, ele não modifica essas variáveis diretamente, mas as copia para a área de armazenamento de trabalho do thread atual. Após a modificação, o valor da variável é substituído pelo valor correspondente. na área de armazenamento principal.
Depois de compreender o modelo de memória Java, não é difícil entender por que n++ não é uma operação atômica. Deve passar por um processo de cópia, adição de 1 e substituição. Este processo é semelhante ao simulado na classe MyThread. Como você pode imaginar, se thread1 for interrompido por algum motivo quando getfield for executado, ocorrerá uma situação semelhante ao resultado da execução da classe MyThread. Para resolver completamente este problema, devemos utilizar algum método para sincronizar n, ou seja, apenas um thread pode operar n ao mesmo tempo, o que também é chamado de operação atômica em n.