Java多线程与并发_CAS详解

真正能让你走远的,都是自律、积极和勤奋

一、什么是CAS?

CAS(CompareAndSwap)
比较当前工作内存中的值和主内存中的值,如果相同则执行规定操作,否则继续比较直到主内存与工作内存中的值一致

CAS应用
CAS有3个操作数,内存值V,旧的预期值A,要修改的更新值B。
当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做

public class CASDemo {

    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(5);

        System.out.println(atomicInteger.compareAndSet(5,2019) + "\t current data: " + atomicInteger.get());
        System.out.println(atomicInteger.compareAndSet(5,1024) + "\t current data: " + atomicInteger.get());
    }
}

结果

true	 current data: 2019
false	 current data: 2019

Process finished with exit code 0

原子整型类设置初始值为5,主存中共享变量值为5,某线程在自己的工作内存进行修改操作值变为2019,在刷新回主内存时,与原先值进行比较,发现还是5,则将主存中值修改为2019,成功;很快第二个线程也想将刚刚操作完的值2014刷回主存,但经过比较,发现值已经发生了变化,不是最初的5而是变成了2019,所以修改失败!

二、原子操作类getAndIncrement()方法详解

atomicInteger.getAndIncrement()方法调用了Unsafe类的getAndAddInt()方法

/**
     * Atomically increments by one the current value.
     *
     * @return the previous value
     * this :当前对象
     * valueOffSet :内存偏移量
     */
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }

1.Unsafe

是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc中,其内部方法操作可以像C指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。
注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务

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;
    }
  • var1 AtomicInteger对象本身
  • var2 该对象值的引用地址
  • var4 需要变动的数量
  • var5 是用过var1 var2找出的主内存中真实的值
  • 用该对象当前的值与var5比较,如果相同,更新var5+var4并且返回true;如果不相同,继续取值然后再比较,直到更新完成

2.原理

1.AtomicInteger里面的value原始值为5,即主内存中AtomicInteger的value为5,根据JMM模型,线程A和线程B各自持有一份值为5的value的副本分别到各自的工作内存。

2.线程A通过getIntVolatile(var1,var2)拿到value值5,这时线程A被挂起。

3.线程B也通过getIntVolatile(var1,var2)方法获取到value值5,此时刚好线程B没有被挂起并执行compareAndSwapInt方法比较内存值也为5,成功修改内存值为6,线程B打完收工,一切OK。

4.这时线程A恢复,执行compareAndSwapInt方法比较,发现自己手里的值数字5与主内存值6不一致,说明该值已经被其他线程抢先一步修改过了,那A线程本次修改失败,只能重新读取重新再来一遍。

5.线程A重新获取value值,因为变量value被volatile修饰,所以其他线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwapInt方法比较,直到成功。

*为什么用CAS不用synchronized?

加锁,同一时间段只能有一个线程来访问,一致性得到了保证,但是并发性下降

3.底层汇编

Unsafe类中的compareAndSwapInt,是一个本地方法,该方法的实现位于unsafe.cpp中

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
  UnsafeWrapper("Unsafe_CompareAndSwapInt");
  oop p = JNIHandles::resolve(obj);
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END

先想办法拿到变量value在内存中的地址
通过Atomic::cmpxchg实现比较替换,其中参数x是即将更新的值,参数e是原内存的值

三、CAS的缺点

1.循环时间长开销大:如果CAS失败,会一直进行尝试,如果CAS长时间一直不成功,可能会给CPU带来很大的开销
2.只能保证一个共享变量的操作:对于多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用加锁来保证原子性
3.ABA问题:(狸猫换太子)如果一个值原来是A,变成B,又变成A,检查时发现它的值没有变化。但实际发生了变化。解决思路是使用版本号,每次变量更新的时把版本号加1,就会变成1A–2B–3C。

AtomicInteger
CAS —> Unsafe —> CAS底层思想 —> ABA —> 原子引用更新 —> 如何规避ABA问题

四、ABA问题

如果一个值原来是A,变成B,又变成A,检查时发现它的值没有变化。但实际发生了变化。

/**
 * @Author: slx
 * @Date: 2019/4/17 17:27
 * 时间戳的原子引用,加版本号
 */
public class ABADemo {  //ABA问题解决       AtomicStampedReference
    static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
    static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100,1);

    public static void main(String[] args) {
        System.out.println("---------------以下是ABA问题的产生------------------");
        new Thread(() -> {
            atomicReference.compareAndSet(100,101);
            atomicReference.compareAndSet(101,100);
        },"thread1").start();

        new Thread(() -> {
            try {
                //暂停t2线程1秒钟,保证上面的t1线程完成了一次ABA操作
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(atomicReference.compareAndSet(100,2019) + "\t" + atomicReference.get());

        },"thread2").start();

        //暂停一会线程
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("---------------以下是ABA问题的解决------------------");
        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "\t第一次版本号:" + stamp + "\t值为:" + atomicStampedReference.getReference());
            //暂停t3线程1秒钟
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            atomicStampedReference.compareAndSet(100,101,1,atomicStampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName() + "\t第二次版本号:" + atomicStampedReference.getStamp() + "\t值为:" + atomicStampedReference.getReference());
            atomicStampedReference.compareAndSet(101,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName() + "\t第三次版本号:" + atomicStampedReference.getStamp() + "\t值为:" + atomicStampedReference.getReference());

        },"thread3").start();

        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "\t第一次版本号:" + stamp + "\t值为:" + atomicStampedReference.getReference());
            //暂停t4线程3秒钟,保证上面的t3线程完成了一次ABA操作
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(atomicStampedReference.compareAndSet(100,2019,stamp,stamp+1) + "\t" + atomicStampedReference.getStamp());
        },"thread4").start();

    }
}

结果

---------------以下是ABA问题的产生------------------
true	2019
---------------以下是ABA问题的解决------------------
thread3	第一次版本号:1	值为:100
thread4	第一次版本号:1	值为:100
thread3	第二次版本号:2	值为:101
thread3	第三次版本号:3	值为:100
false	3

Process finished with exit code 0