[Programação simultânea em oito partes] As três principais características de processos, threads e programação simultânea

Índice

Quais são os conceitos de processo e thread?

Um processo refere-se a um programa em execução. Por exemplo, ao usar o DingTalk ou o navegador, você precisa iniciar este programa, e o sistema operacional alocará determinados recursos para este programa (ocupando recursos de memória). Thread é a unidade básica de escalonamento da CPU. Cada thread executa um determinado fragmento do código de um determinado processo.

Os conceitos de serial, paralelo e simultaneidade?

Serialização significa entrar na fila um por um, e somente o segundo pode ingressar após a conclusão do primeiro.
Paralelismo significa processamento simultâneo.
A simultaneidade aqui não é o problema de alta simultaneidade das três escolas secundárias, mas o conceito de simultaneidade em multi-threading (o conceito de threads de agendamento de CPU). A CPU alterna repetidamente para executar diferentes threads em um período de tempo muito curto. Parece ser paralelo, mas é apenas uma comutação de alta velocidade da CPU.

O paralelismo abrange a simultaneidade.
Paralelismo significa que uma CPU multi-core agenda vários threads ao mesmo tempo, o que significa que vários threads são executados ao mesmo tempo.
Uma CPU de núcleo único não pode obter efeitos paralelos, mas uma CPU de núcleo único é simultaneidade.

Quais são os conceitos de síncrono, assíncrono, bloqueador e não bloqueador?

Síncrono e assíncrono: depois de executar uma determinada função, se o receptor irá feedback ativamente das informações.
Bloqueio e não bloqueio: Após executar uma função, o chamador precisa aguardar feedback sobre o resultado?

Os dois conceitos podem parecer semelhantes, mas o seu foco é completamente diferente.
Bloqueio síncrono : Por exemplo, se você usar uma panela para ferver água, não será avisado quando a água estiver fervendo. Após o início da fervura da água, é necessário esperar que a água ferva.

Sem bloqueio síncrono : por exemplo, se você usar uma panela para ferver água, não será notificado quando a água for fervida. Após o início da fervura da água, não é necessário esperar que a água ferva, você pode realizar outras funções, mas é necessário verificar de vez em quando se a água está fervendo.

Bloqueio assíncrono : Por exemplo, se você ferver água com uma chaleira, depois que a água ferver, você será avisado de que a água está fervendo. Após o início da fervura da água, é necessário esperar que a água ferva.

Não-bloqueio assíncrono : Por exemplo, se você ferver água com uma chaleira, depois que a água ferver, você será notificado de que a água está fervendo. Depois de iniciada a fervura da água, não há necessidade de esperar que a água ferva e você pode realizar outras funções.

O não bloqueio assíncrono tem o melhor efeito. Durante o desenvolvimento normal, a melhor maneira de melhorar a eficiência é usar o não bloqueio assíncrono para lidar com algumas tarefas multithread.

Como os threads são criados?

Herde a classe Thread e substitua o método run

Para iniciar um thread, chame o método start, que criará um novo thread e executará as tarefas do thread. Se você chamar o método run diretamente, isso fará com que o thread atual execute a lógica de negócios no método run.

class Thread implements Runnable {
    
    }
public class MiTest {
    
    
    public static void main(String[] args) {
    
    
        MyJob t1 = new MyJob();
        t1.start();
        for (int i = 0; i < 100; i++) {
    
    
            System.out.println("main:" + i);
        }
    }
}
class MyJob extends Thread{
    
    
    @Override
    public void run() {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            System.out.println("MyJob:" + i);
        }
    }
}
Implemente a interface Runnable e substitua o método run
public class MiTest {
    
    
    public static void main(String[] args) {
    
    
        MyRunnable myRunnable = new MyRunnable();
        Thread t1 = new Thread(myRunnable);
        t1.start();
        for (int i = 0; i < 1000; i++) {
    
    
            System.out.println("main:" + i);
        }
    }
}
class MyRunnable implements Runnable{
    
    
    @Override
    public void run() {
    
    
        for (int i = 0; i < 1000; i++) {
    
    
            System.out.println("MyRunnable:" + i);
        }
    }
}
Implemente Callable, reescreva o método de chamada e coopere com FutureTask

Callable é geralmente usado para métodos de execução sem bloqueio que retornam resultados. Síncrono e sem bloqueio.

public class FutureTask<V> implements RunnableFuture<V> {
    
    }

public interface RunnableFuture<V> extends Runnable, Future<V> {
    
    }
public class MiTest {
    
    
    public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
        //1. 创建MyCallable
        MyCallable myCallable = new MyCallable();
        //2. 创建FutureTask,传入Callable
        FutureTask futureTask = new FutureTask(myCallable);
        //3. 创建Thread线程
        Thread t1 = new Thread(futureTask);
        //4. 启动线程
        t1.start();
        //5. 做一些操作
        //6. 要结果
        Object count = futureTask.get();
        System.out.println("总和为:" + count);
    }
}
class MyCallable implements Callable{
    
    
    @Override
    public Object call() throws Exception {
    
    
        int count = 0;
        for (int i = 0; i < 100; i++) {
    
    
            count += i;
        }
        return count;
    }
}
Crie threads com base no pool de threads

A tarefa é enviada ao conjunto de encadeamentos e o conjunto de encadeamentos cria um encadeamento de trabalho para executar a tarefa.

public class ThreadPoolExecutor extends AbstractExecutorService {
    
    
...
private final class Worker
        extends AbstractQueuedSynchronizer
        implements Runnable
    {
    
    
    ...
}
Classes internas anônimas e abordagem de expressão lambda
// 匿名内部类方式:
  Thread t1 = new Thread(new Runnable() {
    
    
      @Override
      public void run() {
    
    
          for (int i = 0; i < 1000; i++) {
    
    
              System.out.println("匿名内部类:" + i);
          }
      }
  });
  
// lambda方式:
  Thread t2 = new Thread(() -> {
    
    
      for (int i = 0; i < 100; i++) {
    
    
          System.out.println("lambda:" + i);
      }
  });
Resumo: Só existe uma maneira de buscar a camada inferior: implementar Runnble

Quais são os estados dos threads?Quais são os estados dos threads em Java?

Nível do sistema operacional: 5 estados (geralmente para estados de thread tradicionais)
Insira a descrição da imagem aqui
6 estados em Java
Insira a descrição da imagem aqui

Insira a descrição da imagem aqui

  • NOVO : O objeto Thread foi criado, mas o método start ainda não foi executado.

  • RUNNABLE : Quando o objeto Thread chama o método start, ele está no estado RUNNABLE (agendamento de CPU/sem agendamento).

  • BLOQUEADO : sincronizado não obtém o bloqueio de sincronização e está bloqueado.

  • WAITING : Chamar o método wait colocará você no estado WAITING e precisará ser despertado manualmente.

  • TIME_WAITING : Chamar o método sleep ou o método join irá ativar automaticamente, sem necessidade de ativar manualmente.

  • TERMINATED : O método run é executado e o ciclo de vida do thread termina.

BLOCKED , WAITING , TIME_WAITING : todos podem ser entendidos como estados de bloqueio e espera, pois nesses três estados a CPU não irá agendar o thread atual

Exemplos de 6 estados em Java

//NEW:
public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
    });
    System.out.println(t1.getState());
}


//RUNNABLE:
public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        while(true){
    
    
        }
    });
    t1.start();
    Thread.sleep(500);
    System.out.println(t1.getState());
}


//BLOCKED:
public static void main(String[] args) throws InterruptedException {
    
    
    Object obj = new Object();
    Thread t1 = new Thread(() -> {
    
    
        // t1线程拿不到锁资源,导致变为BLOCKED状态
        synchronized (obj){
    
    
        }
    });
    // main线程拿到obj的锁资源
    synchronized (obj) {
    
    
        t1.start();
        Thread.sleep(500);
        System.out.println(t1.getState());
    }
}


//WAITING:
public static void main(String[] args) throws InterruptedException {
    
    
    Object obj = new Object();
    Thread t1 = new Thread(() -> {
    
    
        synchronized (obj){
    
    
            try {
    
    
                obj.wait();
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
    });
    t1.start();
    Thread.sleep(500);
    System.out.println(t1.getState());
}


//TIMED_WAITING:
public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        try {
    
    
            Thread.sleep(1000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    });
    t1.start();
    Thread.sleep(500);
    System.out.println(t1.getState());
}


//TERMINATED:
public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        try {
    
    
            Thread.sleep(500);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    });
    t1.start();
    Thread.sleep(1000);
    System.out.println(t1.getState());
}

Quais são os métodos comuns de threads, com exemplos?

Obtenha o tópico atual

O método estático do thread obtém o objeto thread atual

public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
	// 获取当前线程的方法
    Thread main = Thread.currentThread();
    System.out.println(main);
    // "Thread[" + getName() + "," + getPriority() + "," +  group.getName() + "]";
    // Thread[main,5,main]
}
Defina o nome do tópico

Depois de construir o objeto Thread, certifique-se de definir um nome significativo para solucionar erros posteriormente.

public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        System.out.println(Thread.currentThread().getName());
    });
    t1.setName("模块-功能-计数器");
    t1.start();
}
Definir prioridade do thread

Na verdade, é a prioridade do thread de agendamento da CPU. Existem 10 níveis de prioridade definidos para threads em Java, e qualquer número inteiro é obtido de 1 a 10. Se ultrapassar esta faixa, os erros de exceção de parâmetro serão eliminados.

public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        for (int i = 0; i < 1000; i++) {
    
    
            System.out.println("t1:" + i);
        }
    });
    Thread t2 = new Thread(() -> {
    
    
        for (int i = 0; i < 1000; i++) {
    
    
            System.out.println("t2:" + i);
        }
    });
    t1.setPriority(1);
    t2.setPriority(10);
    t2.start();
    t1.start();
}
Definir concessões para threads

Você pode usar o método estático yield do Thread para alterar o thread atual do estado de execução para o estado pronto.

public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            if(i == 50){
    
    
                Thread.yield();
            }
            System.out.println("t1:" + i);
        }
    });
    Thread t2 = new Thread(() -> {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            System.out.println("t2:" + i);
        }
    });
    t2.start();
    t1.start();
}
Definir suspensão do thread

O método estático sleep de Thread permite que o thread mude do estado de execução para o estado de espera. sleep tem duas sobrecargas de método:

  • O primeiro é modificado por nativo, o que tem o efeito de colocar o thread em estado de espera.
  • O segundo é um método que pode passar em milissegundos e um nanossegundo (se o valor do nanossegundo for maior ou igual a 0,5 milissegundos, adicione 1 ao valor inativo do milissegundo. Se o valor passado em milissegundos for 0 e o valor do nanossegundo não for 0 e, em seguida, durma por 1 milissegundo)
// sleep 会抛出一个 InterruptedException
public static void main(String[] args) throws InterruptedException {
    
    
    System.out.println(System.currentTimeMillis());
    Thread.sleep(1000);
    System.out.println(System.currentTimeMillis());
}
Definir preempção de thread

O método de junção do método não estático do thread precisa ser chamado em um determinado thread.

Se t1.join() for chamado no thread principal, o thread principal entrará no estado de espera e precisará aguardar que todos os threads t1 concluam a execução e, em seguida, retornar ao estado pronto para aguardar o agendamento da CPU.

Se t1.join(2000) for chamado no thread principal, o thread principal entrará no estado de espera e precisará aguardar a execução de t1 por 2 segundos antes de retornar ao estado pronto e aguardar o agendamento da CPU. Se t1 terminou durante o período de espera, o thread principal fica automaticamente pronto e aguarda o escalonamento da CPU.

public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        for (int i = 0; i < 10; i++) {
    
    
            System.out.println("t1:" + i);
            try {
    
    
                Thread.sleep(1000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
    });
    t1.start();
    for (int i = 0; i < 10; i++) {
    
    
        System.out.println("main:" + i);
        try {
    
    
            Thread.sleep(1000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        if (i == 1){
    
    
            try {
    
    
                t1.join(2000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
    }
}
Configurar thread do daemon

Por padrão, os threads são todos threads não-daemon, e a JVM encerrará a JVM atual quando não houver threads não-daemon no programa.
O thread principal é um thread não-daemon por padrão. Se a execução do thread principal terminar, você precisará verificar se há algum thread não-daemon na JVM atual. Se não houver JVM, interrompa-o diretamente.

public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        for (int i = 0; i < 10; i++) {
    
    
            System.out.println("t1:" + i);
            try {
    
    
                Thread.sleep(1000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
    });
    t1.setDaemon(true);
    t1.start();
}
Definir thread em espera e ativação

O thread que adquire o recurso de bloqueio sincronizado pode entrar no pool de espera de bloqueio por meio do método wait e o recurso de bloqueio será liberado.
O thread que adquire o recurso de bloqueio sincronizado pode ativar o thread no pool de espera e adicioná-lo ao pool de bloqueio por meio do método notify ou notifyAll.

notify acorda aleatoriamente um thread no pool de espera para o pool de bloqueio.
notifyAll irá ativar todos os threads no pool de espera e adicioná-los ao pool de bloqueio.

Ao chamar os métodos wait, notify e norifyAll, isso deve ser feito dentro de um bloco ou método de código modificado sincronizado, pois precisa ser operada a manutenção da informação baseada no bloqueio de um determinado objeto.

public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        sync();
    },"t1");

    Thread t2 = new Thread(() -> {
    
    
        sync();
    },"t2");
    t1.start();
    t2.start();
    Thread.sleep(12000);
    synchronized (MiTest.class) {
    
    
        MiTest.class.notifyAll();
    }
}

public static synchronized void sync()  {
    
    
    try {
    
    
        for (int i = 0; i < 10; i++) {
    
    
            if(i == 5) {
    
    
                MiTest.class.wait();
            }
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName());
        }
    } catch (InterruptedException e) {
    
    
        e.printStackTrace();
    }
}

Quais são as maneiras de encerrar um tópico?

Existem muitas maneiras de encerrar um thread. O método mais comumente usado é encerrar o método run do thread, seja ele encerrado por retorno ou lançando uma exceção.

método stop (não usado)

Forçar o encerramento do thread, não importa o que você esteja fazendo, não é recomendado

    @Deprecated
    public final void stop() {
    
    ...}
public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        try {
    
    
            Thread.sleep(5000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    });
    t1.start();
    Thread.sleep(500);
    t1.stop();
    System.out.println(t1.getState());
}
Use variáveis ​​compartilhadas (raramente usadas)

Este método não é muito usado e alguns threads podem continuar executando loops infinitos.
Você pode interromper o loop infinito modificando as variáveis ​​compartilhadas, deixando o thread sair do loop e encerrando o método de execução.

static volatile boolean flag = true;

public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        while(flag){
    
    
            // 处理任务
        }
        System.out.println("任务结束");
    });
    t1.start();
    Thread.sleep(500);
    flag = false;
}
modo de interrupção

Por padrão, há um bit de sinalização de interrupção dentro do bit de sinalização de interrupção do thread: false

Modo variável compartilhada

public static void main(String[] args) throws InterruptedException {
    
    
    // 线程默认情况下,    interrupt标记位:false
    System.out.println(Thread.currentThread().isInterrupted());
    // 执行interrupt之后,再次查看打断信息
    Thread.currentThread().interrupt();
    // interrupt标记位:ture
    System.out.println(Thread.currentThread().isInterrupted());
    // 返回当前线程,并归位为 false,interrupt标记位:ture
    System.out.println(Thread.interrupted());
    // 已经归位了
    System.out.println(Thread.interrupted());

    // =====================================================
    Thread t1 = new Thread(() -> {
    
    
        while(!Thread.currentThread().isInterrupted()){
    
    
            // 处理业务
        }
        System.out.println("t1结束");
    });
    t1.start();
    Thread.sleep(500);
    t1.interrupt();
}

Ao interromper o thread no estado WAITING ou TIMED_WAITING,lançando uma excepção e tratando-a você mesmo.Este
método de parar threads é o mais utilizado,e é também o mais comum em frameworks e JUCs.

public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        while(true){
    
    
            // 获取任务
            // 拿到任务,执行任务
            // 没有任务了,让线程休眠
            try {
    
    
                Thread.sleep(1000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
                System.out.println("基于打断形式结束当前线程");
                return;
            }
        }
    });
    t1.start();
    Thread.sleep(500);
    t1.interrupt();
}

Qual é a diferença entre esperar e dormir?

  • sleep é um método estático na classe Thread e wait é um método na classe Object.
  • sleep pertence a TIMED_WAITING e é despertado automaticamente. wait pertence a WAITING e precisa ser despertado manualmente.
  • O método sleep é executado enquanto mantém o bloqueio e não libera os recursos do bloqueio. Após a execução do método wait, os recursos do bloqueio serão liberados.
  • sleep pode ser executado enquanto mantém o bloqueio ou não. O método wait deve ser executado somente quando houver um bloqueio.

O método wait lançará o thread que contém o bloqueio de _owner para a coleção _WaitSet. Esta operação está modificando o objeto ObjectMonitor. Se o bloqueio sincronizado não for mantido, o objeto ObjectMonitor não poderá ser operado.

Quais são as três principais características da programação simultânea?

  • atomicidade
  • visibilidade
  • Ordem

atomicidade

conceito de atomicidade

JMM (modelo de memória Java). Diferentes hardwares e diferentes sistemas operacionais apresentam certas diferenças nas operações de memória. Para resolver vários problemas que ocorrem com o mesmo código em diferentes sistemas operacionais, Java usa JMM para proteger as diferenças causadas por vários hardwares e sistemas operacionais.

Torne a programação simultânea do Java multiplataforma.

JMM estipula que todas as variáveis ​​​​serão armazenadas na memória principal.Durante a operação, uma cópia precisa ser copiada da memória principal para a memória do thread (memória da CPU) para realizar cálculos dentro do thread. Em seguida, grave-o de volta na memória principal (não necessariamente!).

Definição de atomicidade: Atomicidade significa que uma operação é indivisível e ininterrupta.Quando um thread está em execução, outro thread não o afetará.

A atomicidade da programação simultânea é ilustrada no código:

private static int count;

public static void increment(){
    
    
    try {
    
    
        Thread.sleep(10);
    } catch (InterruptedException e) {
    
    
        e.printStackTrace();
    }
    count++;
}

public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        for (int i = 0; i < 100; i++) {
    
    
           increment();
        }
    });
    Thread t2 = new Thread(() -> {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            increment();
        }
    });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(count);
}

Programa atual: Quando operações multithread compartilham dados, os resultados esperados são inconsistentes com os resultados finais.

Atomicidade: Operações multithread em recursos críticos, os resultados esperados são consistentes com os resultados finais.

Através da análise deste programa, pode-se perceber que a operação de ++ é dividida em três partes: primeiro, o thread obtém os dados da memória principal e os salva no registrador da CPU, depois realiza uma operação +1 no registrar e, finalmente, o resultado Gravar de volta na memória principal.

Garanta a atomicidade da programação simultânea
sincronizado

Porque a operação ++ pode ser visualizada a partir da instrução

imagem.png

Você pode adicionar a palavra-chave sincronizada ao método ou usar um bloco de código sincronizado para garantir a atomicidade

sincronizado pode impedir que vários threads operem recursos críticos ao mesmo tempo.Ao mesmo tempo, apenas um thread operará recursos críticos.

imagem.png

CAS

O que exatamente é CAS?

comparar e trocar é comparação e troca, que é uma primitiva de simultaneidade de CPU.

Ao substituir o valor em um determinado local da memória, ele primeiro verifica se o valor na memória é consistente com o valor esperado e, em caso afirmativo, realiza a operação de substituição. Esta operação é uma operação atômica.

Classes baseadas em inseguros em Java fornecem métodos para operar CAS, e a JVM nos ajudará a implementar os métodos em instruções de montagem CAS.

Mas saiba que CAS é apenas comparação e troca, você mesmo precisa implementar a operação de obtenção do valor original.

private static AtomicInteger count = new AtomicInteger(0);

public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            count.incrementAndGet();
        }
    });
    Thread t2 = new Thread(() -> {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            count.incrementAndGet();
        }
    });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(count);
}

Doug Lea nos ajudou a implementar algumas classes atômicas baseadas em CAS, incluindo o AtomicInteger que vemos agora, e muitas outras classes atômicas...

Desvantagens do CAS : O CAS só pode garantir que a operação de uma variável seja atômica e não pode atingir a atomicidade para múltiplas linhas de código.

Perguntas CAS :

  • Problema ABA : O problema é o seguinte: O número da versão pode ser introduzido para resolver o problema ABA. Java fornece uma operação para anexar números de versão a cada versão quando uma classe está em CAS. Referência AtômicaStampeimagem.png
  • Quando AtomicStampedReference estiver no CAS, ele não apenas julgará o valor original, mas também comparará as informações da versão.
  • public static void main(String[] args) {
          
          
        AtomicStampedReference<String> reference = new AtomicStampedReference<>("AAA",1);
    
        String oldValue = reference.getReference();
        int oldVersion = reference.getStamp();
    
        boolean b = reference.compareAndSet(oldValue, "B", oldVersion, oldVersion + 1);
        System.out.println("修改1版本的:" + b);
    
        boolean c = reference.compareAndSet("B", "C", 1, 1 + 1);
        System.out.println("修改2版本的:" + c);
    }
    
  • O problema do tempo de rotação muito longo :
    • Você pode especificar quantas vezes o CAS fará um loop no total. Se exceder esse número, ele falhará/ou travará diretamente o thread. (Bloqueio de rotação, bloqueio de rotação adaptativo)
    • Após o CAS falhar uma vez, esta operação pode ser armazenada temporariamente.Quando os resultados precisarem ser obtidos posteriormente, todas as operações temporárias podem ser executadas e os resultados finais podem ser retornados.
Bloqueio

Lock foi desenvolvido por Doug Lea em JDK1.5. Seu desempenho é muito melhor do que o sincronizado em JDK1.5. Porém, após a sincronização otimizada do JDK1.6, o desempenho não é muito diferente, mas se envolve Quando há muito de simultaneidade, ReentrantLock é recomendado e o desempenho será melhor.

Método para realizar:

private static int count;

private static ReentrantLock lock = new ReentrantLock();

public static void increment()  {
    
    
    lock.lock();
    try {
    
    
        count++;
        try {
    
    
            Thread.sleep(10);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    } finally {
    
    
        lock.unlock();
    }


}

public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            increment();
        }
    });
    Thread t2 = new Thread(() -> {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            increment();
        }
    });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(count);
}

ReentrantLock pode ser diretamente comparado ao sincronizado. Funcionalmente, ambos são bloqueios.

Mas o ReentrantLock possui funcionalidades mais ricas do que o sincronizado.

A camada inferior do ReentrantLock é implementada com base em AQS e há uma variável de estado mantida com base em CAS para implementar operações de bloqueio.

ThreadLocal

Quatro tipos de referência em Java

Os tipos de referência usados ​​em Java são fortes, suaves, fracos e virtuais .

Usuário usuário = novo usuário();

A coisa mais comum em Java é a referência forte. Quando um objeto é atribuído a uma variável de referência, a variável de referência é uma referência forte. Quando um objeto é referenciado por uma variável de referência forte, ele está sempre em um estado alcançável e é impossível ser reciclado pelo mecanismo de coleta de lixo.Mesmo que o objeto nunca seja usado no futuro, a JVM não o coletará . Portanto, referências fortes são uma das principais causas de vazamentos de memória Java.

SoftReference

Em segundo lugar, existem referências suaves.Para objetos apenas com referências suaves, eles não serão reciclados quando a memória do sistema for suficiente e serão reciclados quando o espaço de memória do sistema for insuficiente. As referências suaves são frequentemente usadas em programas sensíveis à memória como caches.

Depois, há referências fracas, que têm um tempo de vida mais curto do que referências suaves.Para objetos apenas com referências fracas, assim que o mecanismo de coleta de lixo for executado, independentemente de o espaço de memória da JVM ser suficiente, a memória ocupada pelo objeto será sempre recuperado. Pode resolver o problema de vazamento de memória. ThreadLocal resolve o problema de vazamento de memória com base em referências fracas.

Por fim, existe a referência virtual, que não pode ser utilizada sozinha e deve ser utilizada em conjunto com uma fila de referência. A principal função das referências virtuais é rastrear o status dos objetos que estão sendo coletados como lixo. Porém, no desenvolvimento, ainda usamos referências fortes com mais frequência.

A maneira como o ThreadLocal garante a atomicidade é evitar que vários threads operem recursos críticos e permitir que cada thread opere seus próprios dados.

Código

static ThreadLocal tl1 = new ThreadLocal();
static ThreadLocal tl2 = new ThreadLocal();

public static void main(String[] args) {
    
    
    tl1.set("123");
    tl2.set("456");
    Thread t1 = new Thread(() -> {
    
    
        System.out.println("t1:" + tl1.get());
        System.out.println("t1:" + tl2.get());
    });
    t1.start();

    System.out.println("main:" + tl1.get());
    System.out.println("main:" + tl2.get());
}

Princípio de implementação ThreadLocal:

  • Cada Thread armazena uma variável de membro, ThreadLocalMap
  • ThreadLocal em si não armazena dados, é como uma classe de ferramenta que opera ThreadLocalMap com base em ThreadLocal.
  • O próprio ThreadLocalMap é implementado com base em Entry[], porque um thread pode vincular vários ThreadLocals, portanto, vários dados podem precisar ser armazenados, portanto, são implementados na forma de Entry[].
  • Cada ThreadLocalMap existente tem seu próprio ThreadLocalMap independente e, em seguida, usa o próprio objeto ThreadLocal como chave para acessar o valor.
  • A chave do ThreadLocalMap é uma referência fraca. A característica da referência fraca é que mesmo que haja uma referência fraca, ela deve ser reciclada durante o GC. Isso evita que o objeto ThreadLocal seja reciclado se a referência à chave for uma referência forte após o objeto ThreadLocal perder sua referência.

Problema de vazamento de memória ThreadLocal:

  • Se a referência ThreadLocal for perdida, a chave será reciclada pelo GC devido à referência fraca. Se o thread não for reciclado ao mesmo tempo, causará um vazamento de memória e o valor na memória não poderá ser reciclado e não pode ser obtido.
  • Você só precisa chamar o método remove a tempo de remover a entrada após usar o objeto ThreadLocal.

imagem.png

visibilidade

conceito de visibilidade

O problema de visibilidade ocorre com base na localização da CPU. A velocidade de processamento da CPU é muito rápida. Comparada com a CPU, é muito lenta para obter dados da memória principal. A CPU fornece caches de três níveis de L1, L2 e L3. Cada vez que vai para a memória principal, depois que os dados são recuperados da memória, eles serão armazenados no cache de nível 3 da CPU. Cada vez que os dados são recuperados do cache de nível 3, a eficiência certamente melhorará .

Isso trouxe problemas. Hoje em dia, as CPUs são multi-core e a memória de trabalho (cache de terceiro nível da CPU) de cada thread é independente. Ao fazer modificações, cada thread será notificado de que apenas sua própria memória de trabalho será modificada, e não haverá resposta oportuna.Sincronizado com a memória principal, causando problemas de inconsistência de dados.

imagem.png

Lógica de código para problemas de visibilidade

private static boolean flag = true;

public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        while (flag) {
    
    
            // ....
        }
        System.out.println("t1线程结束");
    });

    t1.start();
    Thread.sleep(10);
    flag = false;
    System.out.println("主线程将flag改为false");
}
Maneiras de resolver a visibilidade
volátil

Volátil é uma palavra-chave usada para modificar variáveis ​​de membro.

Se um atributo for modificado como volátil, equivale a informar à CPU que a operação do atributo atual não tem permissão para usar o cache da CPU e deve operar com a memória principal.

Semântica de memória de volátil:

  • O atributo volátil é escrito: Ao escrever uma variável volátil, o JMM atualizará imediatamente o cache da CPU correspondente ao thread atual na memória principal.
  • O atributo volátil é lido: Ao ler uma variável volátil, o JMM definirá a memória correspondente no cache da CPU como inválida e a variável compartilhada deverá ser relida da memória principal.

Na verdade, adicionar volátil é informar à CPU que as operações de leitura e gravação do atributo atual não têm permissão para usar o cache da CPU. O atributo modificado com volátil será anexado com um prefixo de bloqueio após ser convertido em montagem. Quando a CPU executa esta instrução, se prefixá-la com lock fará duas coisas:

  • Grave os dados atuais da linha de cache do processador de volta na memória principal
  • Os dados gravados são diretamente inválidos no cache de outros núcleos da CPU.

Resumo: Volátil significa que toda vez que a CPU operar com esses dados, ela deverá sincronizar imediatamente com a memória principal e ler os dados da memória principal.

private volatile static boolean flag = true;

public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        while (flag) {
    
    
            // ....
        }
        System.out.println("t1线程结束");
    });

    t1.start();
    Thread.sleep(10);
    flag = false;
    System.out.println("主线程将flag改为false");
}
sincronizado

Sincronizado também pode resolver problemas de visibilidade e semântica de memória sincronizada.

Se um bloco de código de sincronização sincronizado ou método de sincronização estiver envolvido, após adquirir o recurso de bloqueio, as variáveis ​​internas envolvidas serão removidas do cache da CPU, e os dados deverão ser recuperados da memória principal, e após o bloqueio ser liberado, a CPU irá imediatamente Os dados no cache serão sincronizados com a memória principal.

private static boolean flag = true;

public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        while (flag) {
    
    
            synchronized (MiTest.class){
    
    
                //...
            }
            System.out.println(111);
        }
        System.out.println("t1线程结束");

    });

    t1.start();
    Thread.sleep(10);
    flag = false;
    System.out.println("主线程将flag改为false");
}
Trancar

A forma como o Lock garante a visibilidade é completamente diferente do sincronizado.Com base em sua semântica de memória, o sincronizado sincroniza o cache da CPU com a memória principal ao adquirir e liberar bloqueios.

O bloqueio é implementado com base em volátil. Ao bloquear e liberar o bloqueio dentro do bloqueio Lock, um atributo de estado modificado por volátil será adicionado ou subtraído.

Se uma operação de gravação for executada em um atributo modificado volátil, a CPU executará a instrução com o prefixo de bloqueio e a CPU sincronizará imediatamente os dados modificados do cache da CPU para a memória principal e também sincronizará imediatamente outros atributos para a memória principal. Esses dados em outras linhas de cache da CPU também serão definidos como inválidos e deverão ser extraídos da memória principal novamente.

private static boolean flag = true;
private static Lock lock = new ReentrantLock();

public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        while (flag) {
    
    
            lock.lock();
            try{
    
    
                //...
            }finally {
    
    
                lock.unlock();
            }
        }
        System.out.println("t1线程结束");

    });

    t1.start();
    Thread.sleep(10);
    flag = false;
    System.out.println("主线程将flag改为false");
}
final

Os atributos finais modificados não podem ser modificados durante a execução. Desta forma, a visibilidade é indiretamente garantida. Todos os atributos finais de leitura multi-thread devem ter o mesmo valor.

Final não significa que cada vez que os dados são recuperados, eles são lidos da memória principal, não é necessário, e final e volátil não podem modificar um atributo ao mesmo tempo.

O conteúdo modificado por final não pode mais ser gravado novamente, e volátil garante que cada dado lido e gravado seja lido da memória principal, e volátil afetará determinado desempenho, portanto não há necessidade de modificá-lo ao mesmo tempo.

imagem.png

Ordem

conceito de ordem

Em Java, o conteúdo do arquivo .java será compilado e precisa ser convertido novamente em instruções que a CPU possa reconhecer antes da execução.Quando a CPU executa essas instruções, a fim de melhorar a eficiência da execução sem afetar o resultado final (satisfatório alguns requisitos), as instruções serão reorganizadas.

A razão pela qual as instruções são executadas fora de ordem é para maximizar o desempenho da CPU.

Os programas em Java são executados fora de ordem.

O programa Java verifica o efeito da execução fora de ordem:

static int a,b,x,y;

public static void main(String[] args) throws InterruptedException {
    
    
    for (int i = 0; i < Integer.MAX_VALUE; i++) {
    
    
        a = 0;
        b = 0;
        x = 0;
        y = 0;

        Thread t1 = new Thread(() -> {
    
    
            a = 1;
            x = b;
        });
        Thread t2 = new Thread(() -> {
    
    
            b = 1;
            y = a;
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        if(x == 0 && y == 0){
    
    
            System.out.println("第" + i + "次,x = "+ x + ",y = " + y);
        }
    }
}

O modo Singleton pode ter problemas devido à reordenação de instruções:

Threads podem obter objetos não inicializados, o que pode causar alguns problemas desnecessários ao usá-los porque as propriedades internas possuem valores padrão.

private static volatile MiTest test;

private MiTest(){
    
    }

public static MiTest getInstance(){
    
    
    // B
    if(test  == null){
    
    
        synchronized (MiTest.class){
    
    

            if(test == null){
    
    
                // A   ,  开辟空间,test指向地址,初始化
                test = new MiTest();
            }
        }
    }
    return test;
}
como se fosse serial

semântica como se fosse serial:

Não importa como a reordenação seja especificada, é necessário garantir que o resultado da execução do programa de thread único permaneça inalterado.

E se houver dependências, as instruções não poderão ser reorganizadas.

// 这种情况肯定不能做指令重排序
int i = 0;
i++;

// 这种情况肯定不能做指令重排序
int j = 200;
j * 100;
j + 100;
// 这里即便出现了指令重排,也不可以影响最终的结果,20100
acontece antes

Regras específicas:
  1. Princípio de acontecer antes de thread único: No mesmo thread, escreva a operação anterior acontecer antes da operação subsequente.
  2. O princípio de acontecer antes das fechaduras: A operação de desbloqueio da mesma fechadura acontece antes da operação de bloqueio desta fechadura.
  3. O princípio da volatilidade acontece antes: Uma operação de gravação em uma variável volátil acontece antes de qualquer operação nesta variável.
  4. O princípio da transitividade do acontecer antes de: Se A opera acontecer antes de B, e B acontecer antes de C, então A acontece antes de C.
  5. O princípio acontecer antes da inicialização do thread: o método de início do mesmo thread acontece antes de outros métodos deste thread.
  6. O princípio da interrupção do thread acontecer antes: A chamada ao método de interrupção do thread acontece antes que o thread interrompido detecte o código enviado pela interrupção.
  7. O princípio do encerramento do thread acontecer antes: Todas as operações em um thread são detectadas antes do encerramento do thread.
  8. O princípio do acontecer antes da criação de objetos: a inicialização de um objeto é concluída antes que seu método finalize seja chamado.
O JMM não acionará o efeito de rearranjo de instruções somente quando as 8 condições acima não ocorrerem.

Você não precisa prestar muita atenção ao princípio do que acontece antes, você só precisa ser capaz de escrever código seguro para threads.

volátil

Se você precisar evitar que o programa reorganize as instruções ao operar um determinado atributo, além de satisfazer o princípio acontece antes, você também pode modificar o atributo com base em volátil, para que o problema de reorganização das instruções não ocorra ao operar este atributo.

Como o volátil proíbe o rearranjo de instruções?

Conceito de barreira de memória. Pense na barreira da memória como uma instrução.

Uma instrução anterior será adicionada entre as duas operações. Esta instrução pode evitar a reordenação de outras instruções executadas para cima e para baixo.

Acho que você gosta

Origin blog.csdn.net/qq_44033208/article/details/132434143
Recomendado
Clasificación