JAVA并发编程:CAS & ABA

生活

如果你交给某人一个程序,你将折磨他一整天;如果你教某人如何编写程序,你将折磨他一辈子。——David Leinweber

CAS是什么?

昨天聊到乐观锁和悲观锁,讲到乐观锁的实现是在多线程下通过CAS不断自旋来更新数据。
那么具体什么事CAS呢,
CAS即compareAndSwap,翻译过来就是比较和交换,
确切的说是比较 旧值与预期值是否一致,如果一致,把值更新为新值,否则不更新。

JAVA中实现CAS

在JAVA里一般通过unsafe来实现cas,这个类里的compareAndSwap方法都是native方法,调用底层的c方法实现。
UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
nextOffset:内存值
cmp:预期值
val:新值

当内存值与预期值一致,即修改为新值。

CAS的应用

在JAVA里,CAS出现在AQS和原子类里。
来看下原子类里CAS的实现:
例AtomicInteger:

public final int getAndSet(int newValue) {
      //自旋
        for (;;) {
            int current = get();
            //CAS
            if (compareAndSet(current, newValue))
                return current;
        }
    }
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

CAS的优缺点

优点:
非阻塞的轻量级乐观锁,在竞争不激烈的情况下,性能要优于synchronized,他有复杂的加锁解锁,还有唤醒等待,涉及到内核态和用户态的转换,性能不高。

缺点:
自旋时间过长会非常消耗CPU资源,在竞争激烈的情况下,性能反而不好。
ABA问题,一个线程获取到变量的值是A,准备把他修改成C,在修改之前,另一个线程进来把值修改成B,又修改成A,此时第一个线程在修改时,发现现在的值还是A,认为没有别的线程修改,于是修改成C成功。

如何避免ABA问题?

ABA的问题在大部分情况下(只涉及到变量本身)是没有问题的,但是也有些情况会出问题。
举个例子:
现有一个链表
A->B->C

线程1:准备把A移除,把B变成头结点。
此时
线程2:把A B 移除,在把A D放入链表 得到链表
B
A->D->C
此时线程1在执行,判断头结点是A没问题,于是
执行
A.next=null;
head=B
最后得到的链表就是
B
丢失了C

解决方式:
可以通过带版本的CAS来避免ABA问题
用AtomicStampedReference/AtomicMarkableReference解决ABA问题
AtomicStampedReference

public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp) {
        Pair<V> current = pair;

// 需要同时判断 版本号 和 值
        return
            expectedReference == current.reference &&
            expectedStamp == current.stamp &&
            ((newReference == current.reference &&
              newStamp == current.stamp) ||
             casPair(current, Pair.of(newReference, newStamp)));
    }

AtomicMarkableReference

扫描二维码关注公众号,回复: 4306892 查看本文章
public boolean compareAndSet(V       expectedReference,
                                 V       newReference,
                                 boolean expectedMark,
                                 boolean newMark) {
        Pair<V> current = pair;
        return
            expectedReference == current.reference &&
            expectedMark == current.mark &&
            ((newReference == current.reference &&
              newMark == current.mark) ||
             casPair(current, Pair.of(newReference, newMark)));
    }

这两者的区别在于:
AtomicStampedReference 关心目标值被动过几次,
AtomicMarkableReference 关心目标值是否被动过?

关于AtomicStampedReference使用的坑

https://blog.csdn.net/xybz1993/article/details/79992120
看到别人写的。看看就好,其实不应该算AtomicStampedReference的坑,只是Integer需要注意的地方。

后记

后面研究阻塞队列和并发容器。

猜你喜欢

转载自blog.csdn.net/qq_28605513/article/details/84642021