0%

CAS

CAS操作,全称为Compare And Swap,现在几乎所有的CPU指令都支持CAS的原子操作,他也是自旋锁,乐观锁的核心操作。

CAS操作的目的,是在一次原子操作中,实现如下功能:

  • 取出某个内存地址存储的值;
  • 比较取出的值是否与预期的值相同;
  • 如果相同则将这个值更新为新的值,不相同则不做更新。

我们用一个例子来解释CAS的适用场景,假设需要统计一个网站的访问次数,那么会有如下代码:

1
2
3
4
5
6
7
public class WebSta {
private volatile int count = 0;

public void clicked() {
count = count + 1;
}
}

如上代码,由于count = count + 1中,会先计算加一之后的值,然后赋值给count,计算和赋值这两步操作并不是原子操作,那么在多线程并发场景下,则统计到的访问次数会比实际次数少。

这是一个很典型的多线程并发的场景,于是我们想到了锁:

1
2
3
4
5
6
7
public class WebSta {
private int count = 0;

public synchronized void clicked() {
count = count + 1;
}
}

这样一来,同时只有一个线程能进入到clicked函数中,便不会出现次数统计的问题,但是引入了性能问题:多线程并发的时候,多个线程会出现排队等待锁的场景,从而导致了挂起/唤醒线程带来的CPU开销,而一行count = count + 1的代码本身是很简单的,挂起/唤醒线程的开销相对来说反而大很多。

AtomicInteger

为了解决这个问题,我们可以将count变量的类型换成AtomicInteger:

1
2
3
4
5
6
7
public class WebSta {
private AtomicInteger count = new AtomicInteger(0);

public void clicked() {
count.getAndAdd(1);
}
}

AtomicInteger是Java中java.util.concurrent包中的原子类,可以利用这个类来解决上述的性能问题,相关源代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class AtomicInteger {
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;

static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}

private volatile int value;

public final int getAndAdd(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta);
}
}

public final class Unsafe {
public native boolean compareAndSwapInt(Object obj, long offset,
int expectedValue, int newValue);
public native boolean compareAndSwapInt(Object obj, long offset,
int expectedValue, int newValue);

public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
}

其中:

  • Unsafe:一个native接口类;
  • valueOffset:存储value成员变量,在当前这个AtomicInteger中的偏移量,即使通过寻址的方式,找到value成员变量的内存其实地址;
  • value:存储具体的int值,声明为volatile来保证线程间可见。

从Unsafe中的代码可以看到,当调用AtomicInteger的getAndAdd时,会先去获取内存对应的值,然后通过compareAndSwapInt方法去修改值(compareAndSwapInt是个native方法,最终会对应到CPU支持的CAS指令),如果修改失败(比如此时有另外一个线程拿到旧值之后,先执行了+1操作,那么这里的compareAndSwapInt中,内存中的值与预期的值v不相等,则更新失败),则重新获取新的值,然后再尝试更新。

java.util.concurrent.atomic包下面,提供了多种数据类型的原子类,也有AtomicReference这种支持对自定义对象进行原子操作的类。

ABA问题

结合上面的例子来说明一下ABA问题是啥。

  • 线程A执行到getIntVolatile一行,假设取出来的值是100;
  • 线程B此时也执行上述逻辑,并将值从100改为200;
  • 线程C紧接着线程B执行上述逻辑,又将值从200改回了100;
  • CPU切回到线程A继续执行,由于此时内存中的值已经回到了100,与刚刚线程A取出来的一致,因此旧值判断成功,更新为新值。

ABA的解决思路,是在给变量加上版本号,在这个例子中,线程A取出来的值为100a,线程B取出来的也是100a,然后修改成200b,线程C取出来的是200b,然后修改成100c,线程A再去执行CAS的时候,发现预期值是100a,实际内存中的值是100c,不匹配,于是更新失败。



-=全文完=-