Se você estiver interessado em aprender mais sobre isso, pode visitar meu site pessoal: Yetong Space
I. Introdução
Vejamos primeiro um caso: desenvolvemos um site e precisamos contar o número de visitas. Cada vez que um usuário enviar uma solicitação, o número de visitas será +1. Como fazer isso? Simulamos que 100 pessoas visitam ao mesmo tempo, e cada pessoa inicia 10 solicitações ao site, e o número total final de visitas deve ser 1000.
O código da simulação é o seguinte:
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class Demo {
// 总访问量
static int count = 0;
// 模拟访问的方法
public static void request() throws InterruptedException {
// 模拟耗时5毫秒
TimeUnit.MILLISECONDS.sleep(5);
count++;
}
public static void main(String[] args) throws InterruptedException {
// 开始时间
long startTime = System.currentTimeMillis();
int threadSize = 100;
CountDownLatch countDownLatch = new CountDownLatch(threadSize);
for (int i = 0; i < threadSize; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
// 模拟用户行为,每个用户访问10次网站
try {
for (int j = 0; j < 10; j++) {
request();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
}
});
thread.start();
}
// 利用countDownLatch实现100个线程结束之后再执行以下代码
countDownLatch.await();
long endTime = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName() + ", 耗时:" + (endTime - startTime) + ", count = " + count);
}
}
Obviamente, a contagem não é 1000, então onde está o problema?
Para o código count++
, ele é realmente executado nas seguintes etapas:
- Obtenha o valor de contagem e escreva-o como A: A=contagem
- Adicione o valor A +1 para obter B: B=A+1
- Atribuir valor B para contar
Se houver dois threads, A e B, executando count++ ao mesmo tempo, eles notificam a execução da primeira etapa das etapas acima e a contagem obtida é a mesma, depois que a operação de 3 etapas é concluída, a contagem é adicionado apenas por 1, resultando em um resultado de contagem incorreto
Então, como resolver este problema? Ao fazer count++
operações, deixamos várias threads entrarem na fila para processamento. request()
Quando várias threads chegam ao método ao mesmo tempo, apenas uma thread pode entrar no método e as outras threads esperam do lado de fora. espere do lado de fora. Em seguida, entre em mais um, para que a operação count++
seja enfileirada e o resultado deve estar correto
Como conseguir o efeito de filas? synchronized
As palavras-chave e palavras-chave em java ReentrantLock
podem bloquear recursos para garantir a correção da simultaneidade.No caso de multi-threading, pode garantir que os recursos bloqueados sejam acessados serialmente.
No código é request()
adicionar synchronized
decorações ao método, consulte meu outro blog para obter detalhes: Programação simultânea - o modelo compartilhado do monitor
Nota-se que após travar o lock, o volume de acesso fica normal, mas também notei que o demorado mudou de 63ms para 5335ms. Isso porque o método é travado e executado serialmente, então demora 5000ms só TimeUnit.MILLISECONDS.sleep(5);
para espere. E o erro de execução resultante é apenas porque count++
ele é acessado simultaneamente, então só podemos count++
bloqueá-lo:
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class Demo {
// 总访问量
volatile static int count = 0;
/**
* @param expectCount 期望值count
* @param newCount 需要给count赋值的新值
* @return boolean
*/
public static synchronized boolean compareAndSwap(int expectCount, int newCount) {
// 判断count当前值是否和期望值expectCount一致,如果一致,则将newCount赋值给count
if(getCount() == expectCount) {
count = newCount;
return true;
}
return false;
}
public static int getCount() {
return count;
}
// 模拟访问的方法
public static void request() throws InterruptedException {
// 模拟耗时5毫秒
TimeUnit.MILLISECONDS.sleep(5);
int expectCount; // 表示期望值
while(!compareAndSwap(expectCount = getCount(), expectCount + 1)) {
}
}
public static void main(String[] args) throws InterruptedException {
// 开始时间
long startTime = System.currentTimeMillis();
int threadSize = 100;
CountDownLatch countDownLatch = new CountDownLatch(threadSize);
for (int i = 0; i < threadSize; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
// 模拟用户行为,每个用户访问10次网站
try {
for (int j = 0; j < 10; j++) {
request();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
}
});
thread.start();
}
// 利用countDownLatch实现100个线程结束之后再执行以下代码
countDownLatch.await();
long endTime = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName() + ", 耗时:" + (endTime - startTime) + ", count = " + count);
}
}
Obviamente, a eficiência deste programa foi muito melhorada.
Dois: Introdução ao CAS
Agora voltando ao assunto, o nome completo do CAS é "Compare And Swap", que significa "Comparar e Substituir". Uma operação CAS consiste em três operandos:
- V: a variável a atualizar (var)
- E: Valor esperado (esperado)
- N: novo valor (novo)
O processo de comparação e troca é o seguinte: julgue se V é igual a E, se for igual, defina o valor de V para N; se não, significa que outros threads já atualizaram V, então o thread atual desiste a atualização e não faz nada. Portanto, o valor esperado E aqui se refere essencialmente ao "valor antigo".
Explicamos esse processo com um exemplo simples:
- Se houver uma variável compartilhada multi-thread i originalmente igual a 5, agora estou no thread A e desejo defini-la como um novo valor de 6, usamos o CAS para fazer isso.
- Primeiro, usamos i para comparar com 5, e descobrimos que é igual a 5, indicando que não foi alterado por outras threads, então defini-o para um novo valor de 6, desta vez o CAS é bem-sucedido e o o valor de i é definido como 6;
- Se não for igual a 5, significa que i foi alterado por outras threads (por exemplo, o valor de i agora é 2), então não farei nada. Desta vez, o CAS falha e o valor de i ainda é 2.
Neste exemplo, i é V, 5 é E e 6 é N.
É possível que o valor de i tenha sido alterado por outros tópicos quando eu estava prestes a atualizar seu novo valor depois de julgar que eu tinha 5 anos? Não vou. Como o CAS é uma operação atômica, é uma primitiva do sistema, uma instrução atômica da CPU, e sua atomicidade é garantida a partir do nível da CPU. Quando vários threads usam o CAS para operar uma variável ao mesmo tempo, apenas um vencerá e atualizará com sucesso e o restante falhará, mas o thread com falha não será suspenso, mas será notificado da falha e poderá tentar novamente. O processo de teste também é chamado de fiação. Obviamente, o thread com falha também pode abortar a operação. É com base nesse princípio que o CAS pode detectar a interferência de outros threads no thread atual, mesmo que não use bloqueios, para lidar com isso em tempo hábil.
Desvantagens do CAS:
- Alta sobrecarga da CPU: Quando a quantidade de simultaneidade é relativamente alta, se muitos threads repetidamente tentarem atualizar uma determinada variável, mas a atualização não for bem-sucedida, o ciclo continua, o que trará grande pressão para a CPU.
- A atomicidade do bloco de código não pode ser garantida: o mecanismo CAS garante apenas a operação atômica de uma variável, mas não pode garantir a atomicidade de todo o bloco de código. Por exemplo, se você precisa garantir que três variáveis sejam atualizadas atomicamente, você deve usar sincronizado.
Três: uso básico
O Java fornece suporte para operações CAS e agora usa o CAS para modificar o exemplo acima:
public class Demo {
// 总访问量
static AtomicInteger count = new AtomicInteger(0);
// 模拟访问的方法
public static void request() throws InterruptedException {
// 模拟耗时5毫秒
TimeUnit.MILLISECONDS.sleep(5);
// 修改访问量
while (true) {
int oldCount = count.get();
int newCount = oldCount + 1;
if (count.compareAndSet(oldCount, newCount)) {
break;
}
}
}
public static void main(String[] args) throws InterruptedException {
long startTime = System.currentTimeMillis();
int threadSize = 100;
CountDownLatch countDownLatch = new CountDownLatch(threadSize);
for (int i = 0; i < threadSize; i++) {
Thread thread = new Thread(() -> {
// 模拟用户行为,每个用户访问10次网站
try {
for (int j = 0; j < 10; j++) {
request();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
});
thread.start();
}
// 利用countDownLatch实现100个线程结束之后再执行以下代码
countDownLatch.await();
long endTime = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName() + ", 耗时:" + (endTime - startTime) + ", count = " + count);
}
}
No entanto, deve-se observar que, ao obter uma variável compartilhada, para garantir a visibilidade da variável, ela precisa ser modificada com volátil. Volátil pode ser usado para modificar variáveis de membros e variáveis de membros estáticos. Pode impedir que threads procurem o valor da variável em seu próprio cache de trabalho, mas obtenham seu valor da memória principal. Threads operam variáveis voláteis diretamente na memória principal. Ou seja, a modificação de uma variável volátil por um thread é visível para outro thread. O CAS deve usar volátil para ler o valor mais recente das variáveis compartilhadas para obter o efeito de comparação e troca. Sobre volátil, você pode ler meu blog em detalhes: Programação Concorrente - Visibilidade e Ordenação