CAS算法中的问题

  • CAS算法中的问题主要有以下三点:
    • ABA问题
    • 循环时间长开销大
    • 只能保证一个共享变量的原子操作

1. ABA问题

  • ABA问题:一个变量V初次读取时为A,准备赋值时检查仍未A,但不能说明它没有被其他线程修改过(可能被修改为其他值后又被改回来了)。
  • ABA问题的解决思路就是在变量前面追加上版本号时间戳。JDK1.5之后的AutomicStampedReference类就是用来解决ABA问题的,其中的compareAndSet()方法会同时比较引用值和标志,只有两个都相等时才会更新引用值和标志
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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) || // 如果新值和旧值相等则直接返回true,不CAS
    casPair(current, Pair.of(newReference, newStamp))); //如果新值和旧值不同,执行CAS
    }

2. 循环时间长开销大

  • CAS经常会用到自旋操作来进行重试,也就是不成功就一直循环执行直到成功。如果长时间不成功,就会增加非常大的CPU的开销,影响系统性能。
  • pause指令可以提升自旋操作的效率,其作用:
    1. 延迟流水线执行指令:pause指令可以延迟指令的执行,从而减少CPU的资源消耗。具体延迟时间取决于处理器的实现版本
    2. 避免内存顺序冲突:在退出循环时,pause指令可以避免由于内存顺序冲突而导致的CPU流水线被清空,从而提高CPU的执行效率

3. 只能保证一个共享变量的原子操作

  • CAS操作仅能对单个共享变量有效。
  • 当需要操作多个共享变量时,CAS就显得无能为力。那么如何解决呢?
    • 从JDK1.5开始,Java提供了AtomicReference类,可以保证引用对象之间的原子性。通过将多个变量封装在一个对象中后可以使用AtomicReference来执行CAS操作。
  • 除了AtomicReference这种方式之外,还可以利用加锁来保证。