CAS(Compare And Swap),比较并交换。
通常用法是:比较内存中某个变量的值是否与预期一致,如果预期一致,则认为可以交换(修改)内存中的值(或认为可以获取到锁)。
应用场景:
cas伪代码:
value = 内存中实际的变量值;
expect = 该变量的预期值;
useValue = 替换后内存中的值;
for(;;){
~
if(value == expect){
value = useValue;
return value ;
}else{
return value ;
}
~
}
cas操作中包括2个部分,1、比较预期值与内存值是否一致,2、当预期一致时,将内存中该变量的值修改。上述两步操作必须是原子的,否则在交换的过程中,内存值已发生变化则该操作毫无意义。
在硬件层面上,处理器架构支持上述cas操作,后面在cas源码会详述。
简单的例子来看cas的应用。静态内部类Entity有个int类型的字段x。cas预期内存值为0 —> 改为3,预期3—> 改为5,预期.—> 改为8。
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
下面结合例子,详述所涉及的知识点:
public final class Unsafe {
private static final Unsafe theUnsafe;
private Unsafe() {
}
@CallerSensitive
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
}
反射获取Unsafe 类的代码
public static Unsafe getUnsafe() {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
return (Unsafe) field.get(null);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public class CasImplTest {
public static void main(String[] args) {
Entity entity = new Entity();
Unsafe unsafe = UnsafeFactory.getUnsafe();
long offset = UnsafeFactory.getFieldOffset(unsafe, Entity.class, "x");
//查看内存结构
ClassLayout layout = ClassLayout.parseInstance(entity);
System.out.println(layout.toPrintable());
System.out.println(offset);
boolean successful;
// 4个参数分别是:对象实例、字段的内存偏移量、字段期望值、字段新值
successful = unsafe.compareAndSwapInt(entity, offset, 0, 3);
System.out.println(successful + "\t" + entity.x);
successful = unsafe.compareAndSwapInt(entity, offset, 3, 5);
System.out.println(successful + "\t" + entity.x);
successful = unsafe.compareAndSwapInt(entity, offset, 3, 8);
System.out.println(successful + "\t" + entity.x);
}
static class Entity{
int x;
}
}
com.huawei.concurrent.cas.CasImplTest$Entity object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 int Entity.x 0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
12
true 3
true 5
false 5
Process finished with exit code 0
通过一个普遍应用的场景来看cas的应用过程。(起10个线程,每个线程自加1w次,需要保证求和结果sum为10w)
此处当然也可以使用synchronized或ReentrantLock来实现锁,但该场景中竞争激烈,如果用synchronized,势必将锁升级到重量级锁,甚至触发park、unpark等内核态操作,效率势必大减。
public class CasTest {
private volatile static int sum = 0;
static CASLock casLock = new CASLock();
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(() -> {
for(;;){
if(casLock.getState()==0 && casLock.cas()) {
try {
for (int j = 0; j < 10000; j++) {
sum++;
}
System.out.println(casLock.getState());
} finally {
casLock.setState(0);
}
break;
}
}
});
thread.start();
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(sum);
}
}
public class CASLock {
//加锁标记
private volatile int state;
private static final Unsafe UNSAFE;
private static final long OFFSET;
static {
try {
UNSAFE = UnsafeFactory.getUnsafe();
OFFSET = UnsafeFactory.getFieldOffset(
UNSAFE, CASLock.class, "state");
} catch (Exception e) {
throw new Error(e);
}
}
public boolean cas() {
return UNSAFE.compareAndSwapInt(this, OFFSET, 0, 1);
}
public int getState() {
return state;
}
public void setState(int state) {
this.state = state;
}
}
尽管拜托了繁杂的锁机制,但cas仍然存在以下问题:
- for(;;)自旋过程中,只有一个线程能够cas成功,其他线程所占CPU一直无休止空转(本文2.3节提供Doug lea的解决方案)
- 只能保证1个共享变量的原子操作(本文2.1节详述解决方案)
- ABA问题(本文1.4节详述问题及解决方案)
ABA问题描述:cas操作前从内存中查到value的值为A,预期内存中该变量的值为A,比较内存值value与A相等,则修改内存值为U。
但假如,在读取A值后,到cas之前,内存中的值发生过2次变化。即:其他线程将内存值value修改为B,而后又改回A,这对当前线程而言是不可见的。如果再继续操作将导致后续数据可能出现脏数据。
ABA问题图解:
ABA问题的解决办法:
CAS操作是乐观锁,每次修改都预期内存值没有发生过改变。这类似于数据库操作中,一种基于数据版本来实现数据同步的机制。简言之,加版本号。
详见本文2.2AtomicStampedReference
核心逻辑:
Hotspot 虚拟机对compareAndSwapInt 方法的实现如下:
#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);
// 根据偏移量,计算value的地址
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
// Atomic::cmpxchg(x, addr, e) cas逻辑 x:要交换的值 e:要比较的值
//cas成功,返回期望值e,等于e,此方法返回true
//cas失败,返回内存中的value值,不等于e,此方法返回false
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
核心逻辑在Atomic::cmpxchg方法中,这个根据不同操作系统和不同CPU会有不同的实现。这里我们以linux_64x的为例,查看Atomic::cmpxchg的实现
#atomic_linux_x86.inline.hpp
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
//判断当前执行环境是否为多处理器环境
int mp = os::is_MP();
//LOCK_IF_MP(%4) 在多处理器环境下,为 cmpxchgl 指令添加 lock 前缀,以达到内存屏障的效果
//cmpxchgl 指令是包含在 x86 架构及 IA-64 架构中的一个原子条件指令,
//它会首先比较 dest 指针指向的内存值是否和 compare_value 的值相等,
//如果相等,则双向交换 dest 与 exchange_value,否则就单方面地将 dest 指向的内存值交给exchange_value。
//这条指令完成了整个 CAS 操作,因此它也被称为 CAS 指令。
__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;
如果多线程竞争激烈,那么cpu狂飙,累加计算的效率却不高。为了解决自旋瓶颈问题,doug lea提供了一个情理之中却又语出惊人的思路,让其他线程不要都只对一个数据累加,线程只要空转,就把线程进行hash加到对应的数组中,分别针对不同的值累加,最终将所有结果求和。
考虑如何提升CPU效率,考虑良久,看到狗哥的思路,直呼惊人的好想法。