JUC并发-CAS原子性操作和ABA问题及解决

概述

  • 乐观锁:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实
  • 悲观锁:也叫独占锁,synchronized就是一种独占锁,会导致其它所有需要锁的线程挂起,等待持有 锁的线程释放锁

乐观锁用到的机制就是CAS,compare and swap

在多线程条件下(A,B两个线程),假如i = 2,我们想通过i++的方式让 i 的值变为4,可能执行不成功,因为i++不是原子性操作,在底层不是表面上的i++一步完成,而是分成几步来操作,所以两个线程很可能拿到的值都是2,最终结果可能为3,所以在进行这种操作的时候,一般是 加锁来实现,但加锁会使效率低下,因此,从jdk5开始,java提供了java.util.concurrent.atomic包来进行原子性操作
在这里插入图片描述
而这里面的原子类的方法都差不多,在不加锁的情况下,都是依靠包装类Unsafe,进行cas操作
我们以AtomicInteger为例做介绍,它定义了一个private volatile int value的变量来存储我们的值,volatile是线程可见的,其中一个线程修改了对其他线程可见,下面我们来演示:

如何进行原子性操作

public class CASDemo { // CAS compareAndSet : 比较并交换!
 public static void main(String[] args) {
  AtomicInteger atomicInteger = new AtomicInteger(2020); 
  // 期望、更新
   //public final boolean compareAndSet(int expect, int update) 
   // 如果我期望的值达到了,那么就更新,否则,就不更新, CAS 是CPU的并发原语! 
   System.out.println(atomicInteger.compareAndSet(2020, 2021)); 
   System.out.println(atomicInteger.get()); atomicInteger.getAndIncrement() 
   System.out.println(atomicInteger.compareAndSet(2020, 2021)); 
   System.out.println(atomicInteger.get());
    } 
}

如代码所示,我们列举几个常用的方法

1.compareAndSet()
AtomicInteger atomicInteger = new AtomicInteger(2020);
atomicInteger.compareAndSet(2020, 2021)

如代码所示,该方法有两个参数,我们进入源码看一下:

 public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

根据源码我们可以知道,第一个参数是期望的值,在new的时候我们传进来的值和该值相比较,如果相等,就更新该原子类的值为第二个参数update的值,**那么该过程是如何实现的呢?**我们继续探究源码:在文章开始就说过,Atomic包的原子性操作依靠的是包装类Unsafe,我们看一下该类都有什么内容(只截取其中一部分):


    public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);

    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

    public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

我们发现该类实现原子操作的方法都加上了native关键字,并且都是compareSwap***的形式,我们将这种原子操作称为CAS 操作,因为java无法直接操作硬件,只能通过调用本地方法即调用C++来完成线程相关的操作,我们继续探究刚才的问题:
unsafe.compareAndSwapInt(this, valueOffset, expect, update);这步是怎样完成的,我们通过源码知道这是本地方法的实现方式,那这些参数有何意义,首先this当前自身对象,valueOffset表示该对象在内存中的地址值,expect代表预期原值,如果当前对象在内存中的值和期望原值相等,就将内存的值改为新值也就是update

2.getAndIncrement()

这个方法和i++的目的一样,使当前的值自增1,具体用法如下:

AtomicInteger atomicInteger = new AtomicInteger(2020);
atomicInteger.getAndIncrement()

AtomicInteger类的值自增1,我们看一下源代码:


public class AtomicInteger extends Number implements java.io.Serializable {
	private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
	...//省略部分代码
	
	public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
    private volatile int value;
}

我们发现是调用unsafe类中的getAndAddInt()方法,而参数valueOffset是获得当前对象的内存偏移值,我们进入这个方法:

public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

我们可以清楚的发现和我们之前介绍的同样用到了compareAndSwapInt()方法,这里不赘述,需要注意的是var5 = this.getIntVolatile(var1, var2);表示从内存地址为var2处获取对象var1的值,接下来,如果和期望原值var5相同,就替换为var5+var4var4就是传下来的参数1,这里通过do... while的方式,是通过一种自旋锁的方式,如果成功就返回结果,否则重试直到成功为止,有兴趣的同学可以去查一下

ABA问题的引出:

java.util.concurrent.atomic包提供了许多可以进行原子更新的类,这些类都使用了unsafe来实现原子操作也就是Compare And Swap(CAS)CAS实现的过程是先取出内存中某时刻的数据,在下一时刻比较并替换,那么在这个时间差会导致数据的变化,此时就会导致出现ABA问题。

  • 简单来说就是:有两个线程A和B,假如A线程想将变量i = 1的值变为2,但此时有另一个线程B在A操作之前先对变量i进行操作了,比如将i变为3,然后再变为1,这时候A查看内存地址中的值还是1,就执行更新操作,尽管线程A操作成功,但是不代表这个过程就是没有问题的。

ABA问题的解决:原子引用

针对ABA问题可能带来隐患,各种乐观锁的实现中通常都会用版本戳version来对记录或对象标记,避免并发操作带来的问题,在Java中,AtomicStampedReference<E>也实现了这个作用,它通过包装[E,Integer]的元组来对对象标记版本戳stamp,从而避免ABA问题
下面举个例子:

public class CASDemo {
    public static void main(String[] args) {
        final AtomicStampedReference<Integer> atomicReference = new AtomicStampedReference<Integer>(1,1);
        new Thread(()->{
            int  stamp =  atomicReference.getStamp();
            System.out.println("A1=>" + stamp);
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            atomicReference.compareAndSet(1,2,atomicReference.getStamp(),
                    atomicReference.getStamp() + 1);
            System.out.println("A2=>" +  atomicReference.getStamp());


            System.out.println(atomicReference.compareAndSet(2, 1, atomicReference.getStamp(),
                    atomicReference.getStamp() + 1));
            System.out.println("A3=>" +  atomicReference.getStamp());

        },"A").start();

        new Thread(()->{
            int  stamp =  atomicReference.getStamp();
            System.out.println("B1=>" + stamp);
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(atomicReference.compareAndSet(1, 6, stamp, stamp + 1));
            System.out.println("B2=>"+atomicReference.getStamp());
        },"B").start();


    }
}

看一下输出结果:

A1=>1
B1=>1
A2=>2
true
A3=>3
false
B2=>3

我们先来解释一下代码,这段代码想表达的是利用stamp来控制版本号,当线程执行某个操作的时候,对应的版本号+1,这里的版本号我们设置的是1,atomicReference.compareAndSet(1,2,atomicReference.getStamp(), atomicReference.getStamp() + 1);,这里在对原有的值进行覆盖的时候,让原来的版本号相应的+1,在A线程睡眠的是后,B线程也拿到了和A线程一样的版本号1,并且这里故意让B睡眠时间长于A,那么A进行了一系列操作,最终使得版本号变为3,这时候B继续执行,用的还是最初的版本号1,这时候问题来了,atomicReference.compareAndSet(1, 6, stamp, stamp + 1)这里的stamp是1,但是真正的版本号stamp已经是3了,那么B线程这句代码就不能执行成功,也就无法对原来的值1进行改变,尽管值1没有发生变化,这里就通过版本戳解决了ABA问题,从本质上看,版本控制也属于乐观锁的实现方式

总结

对于concurrent包的实现:
通过本文我们了解到CAS都是通过内存层面来操作volatile变量,所以Java线程通信大概有以下四种套路:

  • A线程写volatile变量,B线程读这个变量
  • A线程写volatile变量,B线程用CAS更新这个volatile变量
  • A线程用CAS更新一个volatile变量,B线程用CAS更新这个volatile变量
  • A线程用CAS更新一个volatile变量,B线程读这个volatile变量

对于concurrent包下任意的原子类,他们都是先声明共享变量(volatile修饰),然后配合unsafe类的CAS原子操作volatile的可见性来实现线程同步或通信
参考文章:
乐观锁与悲观锁
Java并发:CAS、ABA问题、ABA问题解决方案
Java CAS 和ABA问题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值