🍀介绍
CAS(Compare and swap),即比较并交换。我们平时所说的自旋锁或乐观锁,其中的核心操作实现就是CAS。
🍀保证原子操作
CAS 适用于保证原子操作不被干扰。原子操作即最小不可拆分的操作,也就是说操作一旦开始,就不能被打断,直到操作完成。 在多线程环境下,原子操作是保证线程安全的重要手段。 比如说,假设有两个线程在工作,都想对某个值做修改,就拿自增操作来说吧,要对整数 i 进行自增操作,需要做三个步骤:
- 从内存读取 i 的当前值
- 对 i 值进行加 1 操作
- 将 i 值写回内存
假设两个进程都读取了 i 的当前值,当前值为 0。 这时候 A 线程对 i 加 1 了,B 线程也加 1,最后 i 的是 1 ,而不是 2。
这是因为自增操作不是原子操作,分成的这三个步骤可以被干扰。
再如下面这个例子,10个线程,每个线程都执行 10000次 i++ 操作,我们期望的值是 100000,但是很遗憾,结果总是小于 100000 的。
public class Main {
static int i = 0;
private synchronized static void add() {
i++;
}
private static class plus implements Runnable {
@Override
public void run() {
for (int k = 0; k < 10000; k++) {
add();
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(new plus());
threads[i].start();
}
for (int i = 0; i < 10; i++) {
threads[i].join();
}
System.out.println(i);
}
}
可以加锁或者利用 synchronized 实现:
public synchronized static void add(){
i++;
}
或者,加锁操作,如使用 ReentrantLock (可重入锁)实现。
private static Lock lock = new ReentrantLock();
public static void add(){
lock.lock();
i++;
lock.unlock();
}
🍀CAS 实现自旋锁
既然用锁或 synchronized 关键字可以实现原子操作,那么为什么还要用 CAS 呢,因为加锁或使用 synchronized 关键字带来的性能损耗较大,而用 CAS 可以实现乐观锁,它实际上是直接利用了 CPU 层面的指令,所以性能很高。
上面也说了,CAS 是实现自旋锁的基础,CAS 利用 CPU 指令保证了操作的原子性,以达到锁的效果。自旋就是循环,一般是用无限循环实现。这样一来,一个无限循环中,执行一个 CAS 操作,当操作成功,返回 true 时,循环结束;当返回 false 时,接着执行循环,继续尝试 CAS 操作,直到返回 true。
其实 JDK 中有好多地方用到了 CAS ,上篇博文中说到ConcurrentHashMap中元素添加是线程安全的,就是利用CAS自旋锁实现的。
while ((tab = table) == null || tab.length == 0) {
if ((sc = sizeCtl) < 0)
Thread.yield();
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
其中用到了 compareAndSwapInt 方法,这就是实现 CAS 的核心方法了。 unsafe.compareAndSwapInt(this, valueOffset, e, u) 方法,它是个 native 方法,是用 c++ 实现的。作用在于利用了 CPU 的 cmpxchg 指令完成比较并替换。这也是CAS(比较并交换)的思想,用于保证并发时的无锁并发的安全性。
🍀使用场景
- CAS 适合简单对象的操作,比如布尔值、整型值等
- CAS 适合冲突较少的情况,如果太多线程在同时自旋,那么长时间循环会导致 CPU 开销很大
🍀ABA问题
CAS 存在一个问题,就是一个值从 A 变为 B ,又从 B 变回了 A,这种情况下,CAS 会认为值没有发生过变化,但实际上是有变化的。对此,并发包下倒是有 AtomicStampedReference 提供了根据版本号判断的实现,可以解决一部分问题。
|