Um artigo para entender os conceitos básicos de multithreading Java e o modelo de memória Java


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.

Insira a descrição da imagem aqui

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.
Insira a descrição da imagem aqui
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-00
Thread-01
Thread-02
Thread-03
Thread-04
Thread-10
...

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-028
Thread-029
Thread-030
Thread-130
...

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:

Insira a descrição da imagem aqui
Na figura acima, se você deseja se comunicar entre o encadeamento A e o encadeamento B, siga as duas etapas a seguir:

  1. Primeiro, o segmento A atualiza as variáveis ​​compartilhadas atualizadas na memória local A para a memória principal.
  2. 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:
Insira a descrição da imagem aqui
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
Insira a descrição da imagem aqui

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:

  1. 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.
  2. 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

Publicado 147 artigos originais · 70 elogios · mais de 30.000 visualizações

Acho que você gosta

Origin blog.csdn.net/TreeShu321/article/details/105470007
Recomendado
Clasificación