java中的CAS乐观锁概念以及CAS的问题;如何解决乐观锁ABA问题

Posted 好大的月亮

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java中的CAS乐观锁概念以及CAS的问题;如何解决乐观锁ABA问题相关的知识,希望对你有一定的参考价值。

悲观锁和乐观锁

独占锁是一种悲观锁,·synchronized·就是一种独占锁,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。而另一个更加有效的锁就是乐观锁。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁用到的机制就是CAS,Compare and Swap。

什么是CAS

CAS,compare and swap的缩写

CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该 位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前 值。)CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”

通常将 CAS 用于同步的方式是从地址 V 读取值 A,执行多步计算来获得新 值 B,然后使用 CAS 将 V 的值从 A 改为 B。如果 V 处的值尚未同时更改,则 CAS 操作成功。

CAS存在的问题

CAS虽然很高效的解决原子操作,但是CAS仍然存在三大问题。ABA问题,循环时间长开销大和只能保证一个共享变量的原子操作。

  1. ABA问题。

因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。

从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

现有一个用单向链表实现的堆栈,栈顶为A,这时线程T1已经知道A.next为B,然后希望用CAS将栈顶替换为B:
head.compareAndSet(A,B);
在T1执行上面这条指令之前,线程T2介入,将A、B出栈,再pushD、C、A,此时堆栈结构如下图,而对象B此时处于游离状态:

此时轮到线程T1执行CAS操作,检测发现栈顶仍为A,所以CAS成功,栈顶变为B,但实际上B.next为null,所以此时的情况变为:

其中堆栈中只有B一个元素,C和D组成的链表不再存在于堆栈中,平白无故就把C、D丢掉了。
以上就是由于ABA问题带来的隐患,各种乐观锁的实现中通常都会用版本戳version来对记录或对象标记,避免并发操作带来的问题,在Java中,AtomicStampedReference也实现了这个作用,它通过包装[E,Integer]的元组来对对象标记版本戳stamp,从而避免ABA问题,例如下面的代码分别用AtomicInteger和AtomicStampedReference来对初始值为100的原子整型变量进行更新,AtomicInteger会成功执行CAS操作,而加上版本戳的AtomicStampedReference对于ABA问题会执行CAS失败:

public class Test {
    private static AtomicInteger atomicInt = new AtomicInteger(100);
    private static AtomicStampedReference atomicStampedRef = new AtomicStampedReference(100, 0);
    public static void main(String[] args) throws InterruptedException {
       Thread intT1 = new Thread(new Runnable() {
           @Override
           public void run() {
              atomicInt.compareAndSet(100, 101);
              atomicInt.compareAndSet(101, 100);
           }
       });
       Thread intT2 = new Thread(new Runnable() {
           @Override
           public void run() {
              try {
                  TimeUnit.SECONDS.sleep(1);
              } catch (InterruptedException e) {
              }
              boolean c3 = atomicInt.compareAndSet(100, 101);
              System.out.println(c3); // true
           }
       });
       intT1.start();
       intT2.start();
       intT1.join();
       intT2.join();
       Thread refT1 = new Thread(new Runnable() {
           @Override
           public void run()
              try {
                  TimeUnit.SECONDS.sleep(1);
              } catch (InterruptedException e) {
              }
              atomicStampedRef.compareAndSet(100, 101, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
              atomicStampedRef.compareAndSet(101, 100, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
           }
       });
       Thread refT2 = new Thread(new Runnable() {
           @Override
           public void run() {
              int stamp = atomicStampedRef.getStamp();
              try {
                  TimeUnit.SECONDS.sleep(2);
              } catch (InterruptedException e) {
              }
              boolean c3 = atomicStampedRef.compareAndSet(100, 101, stamp, stamp + 1);
              System.out.println(c3); // false
           }
       });
       refT1.start();
       refT2.start();
    }
}

参考了大佬的博文

https://hesey.wang/2011/09/resolve-aba-by-atomicstampedreference.html添加链接描述

以上是关于java中的CAS乐观锁概念以及CAS的问题;如何解决乐观锁ABA问题的主要内容,如果未能解决你的问题,请参考以下文章

Java并发问题--乐观锁与悲观锁以及乐观锁的一种实现方式-CAS

Java并发问题--乐观锁与悲观锁以及乐观锁的一种实现方式-CAS

乐观锁的一种实现方式——CAS

乐观锁的一种实现方式——CAS

乐观锁实现之CAS算法分析

乐观锁的一种实现方式——CAS