Descriptografando o mecanismo de bloqueio em Java multithreading: os princípios de funcionamento e estratégias de otimização de CAS e Sincronizado

CAS

O que é CAS

CAS: O nome completo é Comparar e trocar, que significa literalmente: "Comparar e trocar". CAS envolve as seguintes operações:
Suponha que os dados originais na memória sejam A, o antigo valor esperado seja B e o valor que precisa ser modificado é C.

  1. Primeiro compare A e B para ver se A e B são iguais.
  2. Se A e B forem iguais, atribua o valor dos dados C a A.
  3. Operação de retorno bem-sucedida.

Vamos escrever um pseudocódigo CAS para nos ajudar a entender melhor o CAS.

 boolean Cas(int a,int b,int c){
    
    
        //进行比较看a是否发生变化
        if(a==b){
    
    
            a=c;
            return true;
        }
       return false;
    }

CAS é um método de implementação de bloqueio otimista.Quando vários threads operam em dados, apenas um thread opera com sucesso e outros threads não serão bloqueados e retornarão um sinal de que a operação falhou.
O CAS real é completado por uma instrução de hardware atômico. Somente quando o hardware o suporta, o software pode ser implementado.

Aplicação de CAS

A biblioteca padrão fornece o pacote java.util.concurrent.atomic, e as classes nele contidas são todas implementadas com base neste método.
Um exemplo típico é a classe AtomicInteger, na qual getAndIncrement é equivalente à operação i++.

public static void main(String[] args) {
    
    
        ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
        AtomicInteger  seq = new AtomicInteger(0);
        //进行++操作
        seq.getAndIncrement();
        seq.getAndIncrement();
        seq.getAndIncrement();
        System.out.println(seq);
    }

Insira a descrição da imagem aqui
Ao clicar no método de incremento automático, vemos que seu funcionamento também é implementado através do pseudocódigo acima.
Insira a descrição da imagem aqui
Você também pode usar CAS para implementar bloqueios de rotação

Problema ABA

Suponha que existam dois threads t1 e t2. Existe uma variável compartilhada num com um valor inicial de A.
Em seguida, o thread t1 deseja usar CAS para alterar o valor num para Z, então ele precisa

  • Primeiro leia o valor de num e registre-o na variável oldNum.
  • Use CAS para determinar se o valor atual de num é A. Se for A, altere-o para Z.
    Porém, entre a execução dessas duas operações por t1, o thread t2 pode alterar o valor de num de A para B e de B para A novamente.

Exemplos de exceção

Tomemos como exemplo a retirada de dinheiro do banco:

  1. Depósito 100, Thread 1 obtém o valor do depósito atual como 100 e espera que ele seja atualizado para 50; Thread 2 obtém o valor do depósito atual como 100 e espera que ele seja atualizado para 50.
  2. O segmento 1 realiza a dedução com sucesso e o depósito é alterado para 50. O thread 2 está bloqueado e aguardando.
  3. Antes da thread 2 ser executada, seu amigo transfere exatamente 50 para você e o saldo da conta passa a ser 100.
  4. É a vez do Thread 2 executar, e descobre-se que o depósito atual é 100, que é igual aos 100 lidos antes, e a operação de dedução é realizada novamente.

Desta forma, o nosso dinheiro desaparecerá, pelo que esta situação é absolutamente inaceitável.

Portanto, introduzimos números de versão para resolver esse problema. CAS também lê o número da versão ao ler o valor antigo. Ao modificar, se o número da versão lida for igual ao número da versão atual, a modificação será feita. Se o número da versão atual for maior que o número da versão lida, a modificação vai falhar.

Princípio sincronizado

Recursos básicos

  1. Começa com o bloqueio otimista e, se o conflito de bloqueio for sério, é atualizado para o bloqueio pessimista.
  2. Sincronizado é um bloqueio reentrante.
  3. É um bloqueio injusto.
  4. É um bloqueio não legível e gravável
  5. Ele começa com uma implementação de trava leve e, se a trava for mantida por muito tempo, ela será convertida em uma trava pesada.

Processo de bloqueio

Fluxograma de bloqueio:
Insira a descrição da imagem aqui

bloqueio de polarização

O bloqueio tendencioso serve para marcar no objeto de bloqueio atual a qual thread o bloqueio pertence. Nenhum bloqueio real é executado. Se puder ser feito sem bloqueio, ele não será bloqueado, reduzindo a sobrecarga desnecessária. Somente quando outros threads competirem pelo bloqueio, o bloqueio será atualizado., de bloqueio tendencioso para bloqueio leve.

fechadura leve

Depois que o bloqueio é atualizado para um bloqueio leve, ele é implementado por meio do CAS.

  • Verifique e atualize um bloco de memória via CAS.
  • Se a atualização for bem-sucedida, o bloqueio será considerado bem-sucedido.
  • Se a atualização falhar, considera-se que o bloqueio falhou e o bloqueio está ocupado.

Bloqueio pesado

Se a competição se tornar mais intensa e o spin não conseguir obter o status de bloqueio rapidamente, ele se expandirá para um bloqueio pesado.O bloqueio
pesado aqui se refere ao uso do mutex fornecido pelo kernel.

  • Para realizar a operação de bloqueio, primeiro insira o estado do kernel.
  • Determine se o bloqueio atual está ocupado no modo kernel
  • Se o bloqueio não estiver ocupado, o bloqueio será bem-sucedido e o sistema voltará ao modo de usuário.
  • Se o bloqueio estiver ocupado, o bloqueio falhará. Nesse momento, o thread entra na fila de espera do bloqueio e trava. Esperando para ser acordado pelo sistema operacional.
  • Após uma série de vicissitudes da vida, o bloqueio foi liberado por outros threads.O sistema operacional também se lembrou do thread suspenso, então acordou o thread e tentou readquirir o bloqueio.

Quando vários threads competem pelo mesmo bloqueio e o tempo de espera de rotação é muito longo e o bloqueio não pode ser obtido, a JVM atualizará o bloqueio para um bloqueio pesado. Neste momento, o thread não gira mais e espera, mas entra no estado do kernel e gerencia o status de bloqueio e a fila de espera por meio da implementação mutex fornecida pelo sistema operacional.
No modo kernel, o sistema operacional determina se o bloqueio atual já está ocupado. Se o bloqueio não estiver ocupado, o thread adquire o bloqueio com sucesso e volta ao modo de usuário para continuar a execução. Se o bloqueio já estiver ocupado, o thread não será bloqueado. Nesse momento, o thread entrará na fila de espera de bloqueio e será suspenso pelo sistema operacional, aguardando para ser despertado.
Conforme o tempo passa e os threads competem, quando outros threads liberam o bloqueio e o sistema operacional percebe que um thread está aguardando o bloqueio, o sistema operacional irá despertar o thread em espera, reiniciá-lo e tentar readquirir o bloqueio. Esse processo pode levar algum tempo, após o qual o thread tenta adquirir o bloqueio novamente para continuar a execução.

Outras operações de otimização

eliminação de bloqueio

O compilador + JVM determina se o bloqueio pode ser eliminado e, em caso afirmativo, é eliminado diretamente.
Em outras palavras, quando muitas de nossas operações de bloqueio são executadas em um único thread, os bloqueios para essas operações de bloqueio não são necessários.

 @Override
    public synchronized StringBuffer append(String str) {
    
    
        toStringCache = null;
        super.append(str);
        return this;
    }

Por exemplo, a operação de acréscimo em StringBuffe envolverá uma operação de bloqueio e podemos eliminar o bloqueio na operação de thread único.

desbaste de bloqueio

Se vários bloqueios e desbloqueios ocorrerem em uma parte da lógica, o compilador + JVM irá tornar os bloqueios automaticamente mais grosseiros.

O exemplo que usamos em aula é:

O líder atribui tarefas às pessoas abaixo. Há três tarefas no total. Agora existem dois métodos:

  1. Faça uma ligação para um funcionário e execute três tarefas ao mesmo tempo.
  2. Faça três ligações para os funcionários, uma tarefa por vez.

Vamos todos escolher, e todos definitivamente escolherão o método 1. É claro que outras JVMs também realizarão esse aumento de bloqueio.

Você pode entender isso com um código:

        //频繁加锁
        for (int i = 0; i < 100; i++) {
    
    
            synchronized (o1){
    
    
            }
        }
        //粗化
        synchronized (o1){
    
    
            for (int i = 0; i < 100; i++) {
    
    
            }
        }

Aperte a trava para evitar aplicar e liberar a trava com frequência.

Acho que você gosta

Origin blog.csdn.net/st200112266/article/details/133085307
Recomendado
Clasificación