CAS (Compare-and-Swap), ou seja, compare e substitua, é uma tecnologia comumente usada na implementação de algoritmos concorrentes.Muitas classes em pacotes simultâneos Java usam a tecnologia CAS. O CAS também é uma pergunta frequente em entrevistas Este artigo irá introduzir os princípios do CAS em profundidade.
1. Exemplo simples
Quando um método implementa i ++, como o método getAndIncrement1 () abaixo, ocorrerão problemas de segurança de simultaneidade quando chamados por vários threads. Neste momento, você pode usar a palavra-chave synchronized para bloquear explicitamente para garantir a segurança de simultaneidade.
No pacote de simultaneidade JAVA, muitas classes atômicas Atomic são fornecidas, o que também pode garantir a segurança do thread, como o método AtomicInteger.getAndIncrement () em getAndIncrement2 ().
package com.wuxiaolong.concurrent;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Description:
*
* @author 诸葛小猿
* @date 2020-09-14
*/
public class Test1 {
public static int i = 0;
/**
* 加锁保证多线程调用的并发安全
* @return
*/
public synchronized static int getAndIncrement1(){
i++;
return i;
}
/**
* AtomicInteger是线程安全的
* @return
*/
public static int getAndIncrement2(){
AtomicInteger ai = new AtomicInteger();
int in = ai.getAndIncrement();
return in;
}
}
2. O que é CAS
CAS: compare e troque ou compare e troque, comparação e interação. A função é garantir que a atualização de um valor por vários encadeamentos seja segura para encadeamentos sem um bloqueio.
Processo CAS: contém três parâmetros CAS (V, E, N), V representa a variável a ser atualizada, E representa o valor esperado e N representa o novo valor. Somente quando o valor V for igual ao valor E, o valor V seja definido como valor N, se o valor V for diferente do valor E, isso significa que outros encadeamentos fizeram atualizações e o encadeamento atual não faz nada.
Se você usar o CAS para implementar a operação i ++, poderá executar as seguintes etapas:
1. Leia o valor atual de i (como i = 0) e marque-o como E
2. Calcule o valor de i ++ como 1, denote-o como V
3. Leia o valor de i novamente e marque-o como N.
4. Se E == N, atualize o valor de i para 1, caso contrário, não atualize.
O processo de CAS é muito semelhante ao bloqueio otimista.O bloqueio otimista acredita que a probabilidade de problemas de segurança de thread é relativamente pequena, portanto, não há necessidade de adicionar bloqueios diretamente, mas comparar os dados originais ao atualizar os dados.
Na quarta etapa acima, se E == N, isso não significa que o valor de i não mudou. Talvez quando um thread executa a quarta etapa, outro thread muda i e, em seguida, muda de volta. Para o primeiro thread diga, Não sei da existência desse processo intermediário. Esse fenômeno é o problema ABA.
Como resolver o problema ABA? Na verdade, é muito simples. Adicione um campo de número de versão a i e adicione 1 ao número de versão sempre que i mudar. Além de comparar o valor de E sempre que i é atualizado, ele também compara se o número da versão é consistente. Isso resolve o problema ABA. No processo de desenvolvimento real, se o problema ABA não tiver impacto nos negócios, não há necessidade de considerar esse problema.
Terceiro, o uso de CAS no AtomicInteger
A implementação subjacente de CAS, a implementação subjacente de synchronized e a implementação subjacente de volatile são todas iguais. Usamos a classe AtomicInteger mencionada acima para ilustrar.
AtomicInteger é seguro para threads e geralmente se diz que AtomicInteger não tem bloqueio ou spinlock. Esta é a aplicação do CAS no JDK.
O código-fonte do método AtomicInteger.getAndIncrement ():
/**
* Atomically increments by one the current value.
*
* @return the previous value
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
Código-fonte Unsafe.getAndAddInt ():
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
Código-fonte Unsafe.compareAndSwapInt ():
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
Este método é native
modificado e a implementação do código-fonte não é visível no JDK. Como o código java é executado na JVM, a JVM da Oracle é Hotspot, se você quiser ver a implementação do método nativo, pode encontrar o código fonte do Hotspot, que é escrito em C e C ++. O código-fonte de Unsafe.java corresponde a unsafe.cpp no código-fonte do Hotspot, que é escrito em C ++.
Quarto, a implementação subjacente do CAS
Para implementar o CAS, você deve entender o código-fonte do Hotspot. Você pode verificar o código do OpenJdk, você pode encontrar o código-fonte de várias versões aqui . Vamos pegar unsafe.cpp em jdk8u como exemplo para continuar o compareAndSwapInt
método de análise .
// 地址:http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/a0eb08e2db5a/src/share/vm/prims/unsafe.cpp
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj);
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END
Você pode ver que o método Atomic :: cmpxchg é chamado, continue a analisar e encontrar este método:
// 地址:http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/a0eb08e2db5a/src/share/vm/runtime/atomic.cpp
jbyte Atomic::cmpxchg(jbyte exchange_value, volatile jbyte* dest, jbyte compare_value) {
assert(sizeof(jbyte) == 1, "assumption.");
uintptr_t dest_addr = (uintptr_t)dest;
uintptr_t offset = dest_addr % sizeof(jint);
volatile jint* dest_int = (volatile jint*)(dest_addr - offset);
jint cur = *dest_int;
jbyte* cur_as_bytes = (jbyte*)(&cur);
jint new_val = cur;
jbyte* new_val_as_bytes = (jbyte*)(&new_val);
new_val_as_bytes[offset] = exchange_value;
while (cur_as_bytes[offset] == compare_value) {
//关键方法
jint res = cmpxchg(new_val, dest_int, cur);
if (res == cur) break;
cur = res;
new_val = cur;
new_val_as_bytes[offset] = exchange_value;
}
return cur_as_bytes[offset];
}
Várias arquiteturas de CPU em vários sistemas têm métodos de implementação relacionados. Os nomes de arquivos específicos são os seguintes:
// 地址:http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/a0eb08e2db5a/src/share/vm/runtime/atomic.inline.hpp
#ifndef SHARE_VM_RUNTIME_ATOMIC_INLINE_HPP
#define SHARE_VM_RUNTIME_ATOMIC_INLINE_HPP
#include "runtime/atomic.hpp"
// Linux
#ifdef TARGET_OS_ARCH_linux_x86
# include "atomic_linux_x86.inline.hpp"
#endif
#ifdef TARGET_OS_ARCH_linux_sparc
# include "atomic_linux_sparc.inline.hpp"
#endif
#ifdef TARGET_OS_ARCH_linux_zero
# include "atomic_linux_zero.inline.hpp"
#endif
#ifdef TARGET_OS_ARCH_linux_arm
# include "atomic_linux_arm.inline.hpp"
#endif
#ifdef TARGET_OS_ARCH_linux_ppc
# include "atomic_linux_ppc.inline.hpp"
#endif
// Solaris
#ifdef TARGET_OS_ARCH_solaris_x86
# include "atomic_solaris_x86.inline.hpp"
#endif
#ifdef TARGET_OS_ARCH_solaris_sparc
# include "atomic_solaris_sparc.inline.hpp"
#endif
// Windows
#ifdef TARGET_OS_ARCH_windows_x86
# include "atomic_windows_x86.inline.hpp"
#endif
// AIX
#ifdef TARGET_OS_ARCH_aix_ppc
# include "atomic_aix_ppc.inline.hpp"
#endif
// BSD
#ifdef TARGET_OS_ARCH_bsd_x86
# include "atomic_bsd_x86.inline.hpp"
#endif
#ifdef TARGET_OS_ARCH_bsd_zero
# include "atomic_bsd_zero.inline.hpp"
#endif
#endif // SHARE_VM_RUNTIME_ATOMIC_INLINE_HPP
No diretório src / os_cpu /, há implementações de código de várias arquiteturas de CPU em vários sistemas. Entre elas, src / os_cpu / linux_x86 / vm é o código baseado na arquitetura x86 no Linux. A implementação final do método cmpxchg:
// 地址:http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/a0eb08e2db5a/src/os_cpu/linux_x86/vm/atomic_linux_x86.inline.hpp
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
int mp = os::is_MP();
__asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
: "=a" (exchange_value)
: "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
: "cc", "memory");
return exchange_value;
}
Uma __asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
parte do código é o núcleo, e asm se refere à linguagem assembly, que é uma linguagem de máquina que interage diretamente com a cpu.
LOCK_IF_MP significa "bloquear se houver várias CPUs" e MP significa Multi-Processadores. O programa decidirá se adiciona o prefixo de bloqueio à instrução cmpxchg de acordo com o número atual de processadores. Se o programa estiver sendo executado em vários processadores, adicione o prefixo de bloqueio (lock cmpxchg) à instrução cmpxchg. Por outro lado, se o programa estiver sendo executado em um único processador, o prefixo de bloqueio é omitido (um único processador manterá a consistência do pedido dentro do único processador, e o efeito de barreira de memória fornecido pelo prefixo de bloqueio não é necessário) .
// 地址:http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/a0eb08e2db5a/src/os_cpu/linux_x86/vm/atomic_linux_x86.inline.hpp
// Adding a lock prefix to an instruction on MP machine
#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "
Resumindo
Como pode ser visto acima, a essência do CAS é:
lock cmpxchg
instrução
Mas cmpxchg
essa instrução de cpu em si não é atômica, ela ainda depende da lock
instrução anterior .
Siga a conta oficial e digite " java-summary " para obter o código-fonte.
Terminado, chame um dia!
[ Disseminação de conhecimento, compartilhamento de valor ], obrigado amigos por sua atenção e apoio, eu sou [ Zhuge Xiaoyuan ], um trabalhador migrante da Internet lutando pela hesitação.