Diretório do artigo
- 1. Ciclo de vida multithread e cinco estados básicos
- Segundo, a criação e o início do multithreading Java
- 1. Herde a classe Thread e reescreva o método run () desta classe
- 2. Crie uma classe de encadeamento implementando a interface Runnable
- 3. Crie um encadeamento através das interfaces Callable e Future
- Três, conceito de modelo de memória Java
- Quarto, a operação interativa entre a memória
- Cinco, a diferença entre volátil e sincronizado
Escrito na frente: a maioria dos alunos que mencionou multithreading pode desaprovar, sem saber o que é multithreading, quando pode ser usado, se há problemas variáveis compartilhados ao usá-lo e assim por diante. Este artigo será dividido em duas partes: a primeira parte é explicar o básico do multithreading e a segunda parte é explicar o modelo de memória Java.
1. Ciclo de vida multithread e cinco estados básicos
Ciclo de vida de multiencadeamento em Java, primeiro observe o diagrama clássico a seguir, que basicamente contém o conhecimento importante de multiencadeamento em Java.
O encadeamento Java possui cinco estados básicos
-
Novo estado (novo): quando o par de objetos do encadeamento é criado, ele entra no novo estado, como: Thread t = new MyThread ();
-
Estado pronto (Executável): quando o método start () do objeto de thread é chamado (t.start ();), o thread entra no estado de pronto. O encadeamento no estado pronto significa que o encadeamento está pronto para aguardar a CPU agendar a execução a qualquer momento, não que o encadeamento seja executado imediatamente após a execução de t.start ();
-
Estado de execução (Em execução): Quando a CPU começa a agendar o encadeamento no estado pronto, o encadeamento pode ser realmente executado nesse momento, ou seja, insira o estado de execução. Nota: O estado pronto é a única entrada no estado em execução, ou seja, se o encadeamento deseja entrar no estado em execução para execução, ele deve primeiro estar no estado pronto;
-
Bloqueado (bloqueado): o encadeamento no estado de execução renuncia temporariamente ao uso da CPU por algum motivo e interrompe a execução.Neste momento, ele entra no estado bloqueado até entrar no estado pronto e, em seguida, tem a oportunidade de ser chamado pela CPU novamente para entrar Para o estado de execução. De acordo com diferentes razões para o bloqueio, o status do bloqueio pode ser dividido em três tipos:
1. Aguardando bloqueio: o segmento no estado de execução executa o método wait () para fazer com que o segmento entre no estado de bloqueio em espera;
2. Bloqueio síncrono - o encadeamento falha ao adquirir o bloqueio sincronizado (porque o bloqueio é ocupado por outros encadeamentos), ele entrará no estado de bloqueio sincronizado
3. Outros bloqueios - chamando o modo de suspensão do encadeamento () ou junção () ou emitindo uma solicitação de E / S, o encadeamento entrará no estado de bloqueio. Quando o estado de suspensão () atinge o tempo limite, join () aguarda o término ou o tempo limite do encadeamento, ou o processamento de E / S é concluído, o encadeamento volta ao estado de pronto.
Dead (Dead): O thread finaliza a execução ou sai do método run () devido a uma exceção, e o thread termina seu ciclo de vida.
Segundo, a criação e o início do multithreading Java
Existem três formas básicas de criação de encadeamentos em Java
1. Herde a classe Thread e reescreva o método run () desta classe
Herdar a classe Thread e reescrever o método run () dessa classe
public class MyThread extends Thread{
@Override
public void run() {
for (int i = 0 ;i < 50;i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
public static void main(String[] args) {
for (int i = 0;i<50;i++) {
//调用Thread类的currentThread()方法获取当前线程
System.out.println(Thread.currentThread().getName() + " " + i);
if (i == 10) {
new MyThread().start();
new MyThread().start();
}
}
}
}
Resultado da operação:
...
main 48
main 49
Thread-0:0
Thread-0:1
Thread-0:2
Thread-0:3
Thread-0:4
Thread-1:0
...
Este é o resultado após a execução do código, como pode ser visto na figura:
1. Existem três threads: main, Thread-0, Thread-1
2. O valor da variável de membro i emitida pelos dois threads Thread-0 e Thread-1 não é contínuo (onde i é uma variável de instância em vez de uma variável local). Porque: ao implementar multithreading herdando a classe Thread, a criação de cada thread deve criar objetos de subclasse diferentes, resultando nos dois threads Thread-0 e Thread-1 não podem compartilhar a variável de membro i;
3. A execução do encadeamento é preventiva e não diz que o Thread-0 ou o Thread-1 sempre ocupou a CPU (isso também está relacionado à prioridade do encadeamento. Aqui, a prioridade do encadeamento de Thread-0 e Thread-1 é a mesma, conhecimento sobre a prioridade de encadeamento Não expanda aqui)
2. Crie uma classe de encadeamento implementando a interface Runnable
Defina uma classe para implementar a interface Runnable; crie um objeto de instância obj desta classe; passe obj como um parâmetro construtor no objeto de instância da classe Thread, este objeto é o objeto de thread real
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0 ;i < 50 ;i++) {
System.out.println(Thread.currentThread().getName()+":" +i);
}
}
public static void main(String[] args) {
for (int i = 0;i < 50;i++) {
System.out.println(Thread.currentThread().getName() + ":" +i);
if (i == 10) {
MyRunnable myRunnable = new MyRunnable();
new Thread(myRunnable).start();
new Thread(myRunnable).start();
}
}
//java8 labdam方式
new Thread(() -> {
System.out.println(Thread.currentThread().getName());
},"线程3").start();
}
}
Resultado da operação:
...
main:46
main:47
main:48
main:49
Thread-0:28
Thread-0:29
Thread-0:30
Thread-1:30
...
1. A variável de membro i emitida pelo encadeamento 1 e encadeamento 2 é contínua, o que significa que a criação de um encadeamento dessa maneira pode permitir que vários encadeamentos compartilhem a variável de instância da classe de encadeamento, porque vários encadeamentos aqui usam a mesma instância de destino Variável. No entanto, ao executar o código acima, você descobrirá que os resultados não são realmente contínuos, pois quando vários threads acessam o mesmo recurso, se o recurso não estiver bloqueado, haverá problemas de segurança do thread (isso é Conhecimento sobre sincronização de encadeamentos, não expandido aqui);
2, java8 pode usar lambda para criar vários encadeamentos.
3. Crie um encadeamento através das interfaces Callable e Future
Crie uma classe de implementação de interface Callable e implemente o método call (), que atuará como o corpo de execução do encadeamento, e o método terá um valor de retorno e, em seguida, crie uma instância da classe de implementação Callable; use a classe FutureTask para agrupar o objeto Callable, que encapsula esse O valor de retorno do método call () do objeto Callable; use o objeto FutureTask como o destino do objeto Thread para criar e iniciar um novo thread; chame o método get () do objeto FutureTask para obter o valor de retorno após a execução do thread filho
public class MyCallable implements Callable<Integer> {
private int i = 0;
@Override
public Integer call() throws Exception {
int sum = 0;
for (; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
sum += i;
}
return sum;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 创建MyCallable对象
Callable<Integer> myCallable = new MyCallable();
//使用FutureTask来包装MyCallable对象
FutureTask<Integer> ft = new FutureTask<Integer>(myCallable);
for (int i = 0;i<50;i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
if (i == 30) {
Thread thread = new Thread(ft);
thread.start();
}
}
System.out.println("主线程for循环执行完毕..");
Integer integer = ft.get();
System.out.println("sum = "+ integer);
}
}
O tipo de valor de retorno do método call () é igual ao tipo em <> quando o objeto FutureTask é criado.
Três, conceito de modelo de memória Java
Na programação simultânea, precisamos lidar com duas questões principais: como se comunicar entre os threads e como sincronizar entre os threads (os threads aqui se referem à execução simultânea de entidades ativas). Comunicação refere-se ao mecanismo pelo qual os threads trocam informações. Na programação imperativa, existem dois mecanismos de comunicação entre os threads: memória compartilhada e passagem de mensagens.
No modelo simultâneo de memória compartilhada, os threads compartilham o estado comum do programa e os threads se comunicam implicitamente, escrevendo-lendo o estado comum na memória. No modelo de simultaneidade de passagem de mensagens, não há estado comum entre os threads, e os threads devem se comunicar explicitamente enviando mensagens explicitamente.
A memória de heap é compartilhada entre threads (este artigo usa o termo "variáveis compartilhadas" para se referir a domínios de instância, domínios estáticos e elementos de matriz). Variáveis locais, parâmetros de definição de método (chamados parâmetros formais de método na especificação da linguagem Java) e parâmetros do manipulador de exceção não serão compartilhados entre encadeamentos, eles não terão problemas de visibilidade de memória nem Influenciado pelo modelo de memória.
Interpretação da memória principal e da memória de trabalho
Memória principal: a área em que existem instâncias de uma classe.Todas as instâncias existem na memória principal e o campo da instância também está localizado aqui. A memória principal é compartilhada por todos os encadeamentos e a memória principal corresponde principalmente à parte de dados da instância dos objetos no heap Java.
Memória de trabalho: cada thread possui sua própria área de trabalho.Na memória de trabalho, há uma cópia parcial da memória principal, chamada cópia de trabalho.
A comunicação entre os encadeamentos Java é controlada pelo modelo de memória Java (referido como JMM neste artigo) O JMM determina quando a gravação de variáveis compartilhadas de um encadeamento é visível para outro encadeamento. De uma perspectiva abstrata, o JMM define o relacionamento abstrato entre os threads e a memória principal: variáveis compartilhadas entre os threads são armazenadas na memória principal e cada thread possui uma memória local privada (memória local) , A memória local armazena o encadeamento para ler / gravar uma cópia da variável compartilhada. A memória local é um conceito abstrato de JMM e realmente não existe. Ele abrange armazenamento em cache, buffers de gravação, registradores e outras otimizações de hardware e compilador. O diagrama esquemático abstrato do modelo de memória Java é o seguinte:
Na figura acima, se você deseja se comunicar entre o encadeamento A e o encadeamento B, siga as duas etapas a seguir:
- Primeiro, o segmento A atualiza as variáveis compartilhadas atualizadas na memória local A para a memória principal.
- Em seguida, o encadeamento B vai para a memória principal para ler a variável compartilhada que o encadeamento A atualizou anteriormente.
A seguir, ilustramos as duas etapas de um diagrama esquemático:
Como mostrado na figura acima, as memórias locais A e B têm uma cópia da variável compartilhada x na memória principal. Suponha que, inicialmente, os valores x nas três memórias sejam 0.
1. Durante a execução do encadeamento A, o valor x atualizado (assumindo o valor 1) é temporariamente armazenado em sua memória local A. Quando o segmento A e o segmento B precisam se comunicar, o segmento A atualiza primeiro o valor x modificado na memória local para a memória principal, momento em que o valor x na memória principal se torna 1.
2. O encadeamento B vai para a memória principal para ler o valor x atualizado do encadeamento A. Nesse momento, o valor x da memória local do encadeamento B também se torna 1.
Vistas como um todo, essas duas etapas são essencialmente o segmento A, enviando mensagens para o segmento B, e esse processo de comunicação deve passar pela memória principal. O JMM fornece garantias de visibilidade da memória para programadores java, controlando a interação entre a memória principal e a memória local de cada encadeamento.
Quarto, a operação interativa entre a memória
A interação entre a memória principal e a memória de trabalho define 8 operações atômicas. Os detalhes são os seguintes:
-
lock: Uma variável que atua na memória principal e identifica uma variável como um estado exclusivo de thread
-
unlock (unlock): uma variável que atua na memória principal e libera uma variável que está no estado bloqueado
-
read (read): uma variável que atua na memória principal, transfere o valor de uma variável da memória principal para a memória de trabalho do encadeamento
-
load: uma variável que atua na memória de trabalho, coloca ou copia o valor da variável transferida da leitura para uma cópia da variável na memória de trabalho
-
use (use): uma variável que atua na memória de trabalho, o que significa que o encadeamento se refere ao valor da variável na memória de trabalho e transmite o valor de uma variável na memória de trabalho ao mecanismo de execução
-
assign (assignment): uma variável que atua na memória de trabalho, indicando que o thread atribui o valor especificado a uma variável na memória de trabalho.
-
store (storage): uma variável que atua na memória de trabalho e transfere o valor de uma variável na memória de trabalho para a memória principal
-
write: escreve para a variável na memória principal, coloca o valor da variável passado pelo armazenamento na variável correspondente na memória principal
A imagem abaixo pode nos ajudar a aprofundar nossa impressão
Cinco, a diferença entre volátil e sincronizado
Primeiro, precisamos entender dois aspectos da segurança do encadeamento: controle de execução e visibilidade da memória.
O objetivo do controle de execução é controlar a execução do código (sequência) e se ela pode ser executada simultaneamente.
A visibilidade da memória controla a visibilidade dos resultados da execução do encadeamento para outros encadeamentos na memória. De acordo com a implementação do modelo de memória Java, quando o encadeamento é executado especificamente, ele primeiro copia os dados da memória principal no local do encadeamento (cache da CPU) e depois libera o resultado do encadeamento local na memória principal após a conclusão da operação.
A palavra - chave sincronizada resolve o problema do controle de execução, impedindo que outros threads adquiram o bloqueio de monitoramento do objeto atual, para que o bloco de código protegido pela palavra-chave sincronizada no objeto atual não possa ser acessado por outros threads e não possa ser executado simultaneamente. Mais importante, a sincronização também criará uma barreira de memória.A instrução de barreira de memória garante que todos os resultados da operação da CPU sejam diretamente liberados para a memória principal, garantindo assim a visibilidade da operação e também fazendo com que todos os threads que primeiro adquirem esse bloqueio Operação, acontece antes na operação do segmento que posteriormente adquiriu a trava.
Para entender o que acontece antes, consulte este artigo [Princípios Importantes da Concorrência], entender e aplicar o que acontece antes
A palavra-chave volátil resolve o problema de visibilidade da memória, para que todas as leituras e gravações em variáveis voláteis sejam diretamente escovadas na memória principal, o que garante a visibilidade das variáveis. Dessa forma, você pode atender a alguns requisitos que exigem visibilidade variável e sem requisitos de ordem de leitura.
O uso da palavra-chave volátil pode apenas perceber a atomicidade das operações nas variáveis originais (como boolen, short, int, long etc.), mas precisa de atenção especial que o volátil não pode garantir a atomicidade das operações compostas.
Para a palavra-chave volátil, ela pode ser usada se e somente se todas as seguintes condições forem atendidas:
- A gravação em uma variável não depende do valor atual da variável ou você pode garantir que apenas um único encadeamento atualize o valor da variável.
- A variável não é incluída na invariante com outras variáveis
A diferença entre volátil e sincronizado
- A essência do volátil é dizer à jvm que o valor da variável atual no registro (memória de trabalho) é incerto e precisa ser lido da memória principal; sincronizado é bloquear a variável atual, apenas o thread atual pode acessar a variável e outros threads são bloqueados .
- volátil pode ser usado apenas no nível da variável; sincronizado pode ser usado no nível da variável, método e classe
- O volátil pode apenas alcançar a visibilidade da modificação das variáveis e não pode garantir a atomicidade; enquanto o sincronizado pode garantir a visibilidade da modificação e a atomicidade das variáveis
- volátil não causa bloqueio de linha; sincronizado pode causar bloqueio de linha.
- Variáveis marcadas com volátil não serão otimizadas pelo compilador; variáveis marcadas com sincronizadas podem ser otimizadas pelo compilador.
Este artigo refere-se a: Conhecimento aprofundado do modelo de memória Java (1)
Se você deseja entender o perigo do padrão de bloqueio com verificação dupla, consulte este artigo: [Modo de caso único] Problemas e soluções de DCL