Índice
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.
- Primeiro compare A e B para ver se A e B são iguais.
- Se A e B forem iguais, atribua o valor dos dados C a A.
- 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);
}
Ao clicar no método de incremento automático, vemos que seu funcionamento também é implementado através do pseudocódigo acima.
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:
- 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.
- O segmento 1 realiza a dedução com sucesso e o depósito é alterado para 50. O thread 2 está bloqueado e aguardando.
- Antes da thread 2 ser executada, seu amigo transfere exatamente 50 para você e o saldo da conta passa a ser 100.
- É 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
- Começa com o bloqueio otimista e, se o conflito de bloqueio for sério, é atualizado para o bloqueio pessimista.
- Sincronizado é um bloqueio reentrante.
- É um bloqueio injusto.
- É um bloqueio não legível e gravável
- 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:
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:
- Faça uma ligação para um funcionário e execute três tarefas ao mesmo tempo.
- 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.