深入理解CAS问题与ABA问题

671 阅读3分钟

这是我参与8月更文挑战的第1天,活动详情查看:8月更文挑战

前言 在JDK 5之前Java语言是靠synchronized关键字保证同步的,这会导致有锁 synchronized关键字会让没获得锁资源的线程进入BLOCKED(阻塞)状态,只有在争夺到锁资源的时候才转换成RUNNABLE(运行)状态。这其中涉及到操作系统中用户模式和内核模式之间的切换,代价比较高。

同时,尽管jdk对synchronized关键字进行了优化,增加了从偏向锁到轻量级锁再到重量级锁的过度,但是在最终转变为重量级锁之后,性能依然比较低,所以面对这种只对单个变量进行原子性的操作,最好使用jdk自带的“原子操作类”。 锁机制存在以下问题:

(1)在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。

(2)一个线程持有锁会导致其它所有需要此锁的线程挂起。

(3)如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。

volatile是不错的机制,但是volatile不能保证原子性。因此对于同步最终还是要回到锁机制上来。

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

1.什么是cas?

CAS,compare and swap的缩写,中文翻译成比较并交换。 下面是原子整型类的CAS实现:

public static void main(String[] args) {
    AtomicInteger atomicInteger = new AtomicInteger(3);
    System.out.println(atomicInteger.compareAndSet(3,2021));
    System.out.println(atomicInteger.compareAndSet(3,2021));
}

这段代码深刻地诠释了比较并交换 当内存里面值为3的时候 就赋值为2021,成功返回true,失败返回false 返回结果如下:

true

false

2.CAS的缺点:

1.CPU开销过大

自循环时间长,开销大

2.ABA问题

简单的来说就是有三个线程 第一个线程将变量由 1 改为 2 第二个线程 由2改为1 第三个线程由 1改为2。 例如 我要去银行取钱 我有存款一百元 我现在要去取款五十元,但是取款机硬件出现了问题,我提交了两遍,也就是说 我开起了两个线程 两个线程都是要 获取到100元 然后更改为50元,但是在执行的时候,有一个线程由于未知原因进入了阻塞状态,有一个线程成功了,这个时候余额被改为了50元,就在这时,有人又给我汇款了五十块钱,我的余额又变成了100块钱。然后刚才被阻塞的线程又恢复了,compare到余额还是一百,所以又执行成功了 将余额改为了五十。 本来阻塞的那个线程应该是失败的,我的余额应该是100块钱,现在因为ABA问题导致成功执行了。

2.ABA问题解决办法

双重判断,利用AtomicStampedReference通过比较值和版本号来更新值,也就是传说中的乐观锁

//正常在业务操作,这里面比较的都是一个个对象
AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(1, 1);

new Thread(()->{
    int stamp = atomicStampedReference.getStamp();
    System.out.println("版本号a1=>"+stamp);
    atomicStampedReference.compareAndSet(1,2,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
    System.out.println("版本号a2=>"+atomicStampedReference.getStamp());
    try {
        TimeUnit.SECONDS.sleep(2);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    atomicStampedReference.compareAndSet(2,1,atomicStampedReference.getStamp(), atomicStampedReference.getStamp()+1);
    System.out.println("版本号a3=>"+atomicStampedReference.getStamp());

},"a").start();

//乐观锁的原理相同!
new Thread(()->{
    int stamp = atomicStampedReference.getStamp();
    System.out.println("版本号b1=>"+stamp);

    try {
        TimeUnit.SECONDS.sleep(2);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    atomicStampedReference.compareAndSet(1,6,stamp,stamp+1);
    System.out.println("版本号b2=>"+atomicStampedReference.getStamp());

},"b").start();

运行代码:

版本号a1=>1
版本号a2=>2
版本号b1=>2
版本号b2=>3
版本号a3=>3