Programação simultânea - CAS

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);
    }
}

insira a descrição da imagem aqui

Obviamente, a contagem não é 1000, então onde está o problema?

Para o código count++, ele é realmente executado nas seguintes etapas:

  1. Obtenha o valor de contagem e escreva-o como A: A=contagem
  2. Adicione o valor A +1 para obter B: B=A+1
  3. 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? synchronizedAs palavras-chave e palavras-chave em java ReentrantLockpodem 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 synchronizeddecorações ao método, consulte meu outro blog para obter detalhes: Programação simultânea - o modelo compartilhado do monitor

insira a descrição da imagem aqui
insira a descrição da imagem aqui
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.
insira a descrição da imagem aqui

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:

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

Acho que você gosta

Origin blog.csdn.net/tongkongyu/article/details/129350613
Recomendado
Clasificación