CAS的概述

CAS的底层原理

Unsafe类+CAS思想(自旋锁)

CAS介绍

  • CAS全称为Compare-And-Swap,它是一条cpu并发原语,比较工作内存值(预期值)和物理内存的共享值是否相同,相同则执行规定操作,否则继续比较,直到工作内存和主内存的值相同,这个过程是原子性的。
  • Atomic相关的类主要是利用CAS、volatile和native方法来保证原子操作的,从而避免synchronized这样的重锁产生的高开销,执行效率大大提升。
8574d746789c4b93896a507709dc2dc6.png
  • CAS并发原语体现在Java语言中的sun.misc包下的Unsafe类的各种方法,调用Unsafe类中的CAS方法,JVM会帮我们实现CAS汇编指令,完全依赖于硬件功能,通过它实现了原子操作,再次强调,==由于CAS是一种系统原语,原语属于操作系统范畴,是由若干条指令组成,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许中断,也就是说CAS是一条原子指令,不会造成所谓的数据不一致的问题。==

关于Unsafe.getAndIncrement()方法分析

2886a730884f452bb514085a67c0b04d.png

CAS代码展示

/*
* CAS:Compare and swap [比较并交换]
* */
public class AtomicIntegerDemo {
	public static void main(String[] args) {
		AtomicInteger atomicInteger=new AtomicInteger(5);
		//true 2019
		System.out.println(atomicInteger.compareAndSet(5, 2019)+"\t"+atomicInteger.get());
		//false 2019
		System.out.println(atomicInteger.compareAndSet(5, 2222)+"\t"+atomicInteger.get());
	}
}

Unsafe类

Unsafe类的介绍

  • Unsafe类是CAS的核心类,由于Java方法无法直接访问底层,是需要通过本地的native方法进行访问,Unsafe相当于一个后门,基于该类可以直接操作特定的内存数据,Unsafe类位于sun.misc包中,其内部方法可以像C的指针一样直接操作内存,因此Java中CAS操作依赖于Unsafe类的方法。
  • Unsafe类中的所有方法都有native修饰,所以Unsafe类中的方法都可以直接操作底层资源,执行响应任务。
    4abecaecbc0949dc894a3a1846e87752.png

变量valueOffset

该变量是内存中的偏移地址,因为Unsafe类就是根据内存偏移地址来获取数据的。
291fda52c8b941a7bd240c8dcfea7de0.png

下图中的value被volatile修饰,保证了多线程之间的可见性。
9293dbaca67a4a349061a44f5b131973.png

CAS的缺点

第一点

循环时间长的话,开销很大,可以看到getAndInt方法执行时,有个do while,如果CAS失败,会一直进行尝试,如果CAS一直不成功,可能给CPU带来很大的开销。
1f7108a17c3c43c191db626a5f3a5c4f.png

第二点

  • 只能保证一个共享变量的原子性
    • 当对一个共享变量执行操作时,可以使用循环CAS来保证原子性;
    • 当对多个共享变量执行操作时,循环CAS就无法保证操作的原子性,这个时候,可以用锁来保证原子性。

第三点--ABA问题的产生(重点)

ABA问题说明

比如一个线程t1从内存位置V取出值A,这时候另一个线程t2也从内存中取出值A,并且线程t2进行了一些操作将值A变为值B,又将V位置的数据变回了值A,这时候t1进行CAS操作时发现内存中仍然是A,然后线程t1执行成功,尽管t1执行成功,并不代表过程没有问题。

ABA代码演示

//ABA问题
 static AtomicInteger atomicInteger = new AtomicInteger(100);

    private static void extracted() {
        new Thread(() -> {
            //先改为101
            atomicInteger.compareAndSet(100, 101);
            try {
                //确保第一次修改完,往下执行
                TimeUnit.MILLISECONDS.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //又改回100
            atomicInteger.compareAndSet(101, 100);
        }, "t1").start();
        new Thread(() -> {
            try {
                //确保线程1发生了ABA问题
                TimeUnit.MILLISECONDS.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
			//结果:true  2022
            System.out.println(atomicInteger.compareAndSet(100, 2022) + "\t" + atomicInteger.get());
        }, "t2").start();
    }

ABA问题解决方案

解决方法

ABA问题解决方案可以使用AtomicStampedReference,每修改一次都会有一个版本号。

解决方法代码演示
/**
     * 初始值100,initialStamp版本号1
     */
    static AtomicStampedReference asr = new AtomicStampedReference(100, 1);

    public static void main(String[] args) {
        new Thread(() -> {
            //首次版本号
            int stamp = asr.getStamp();
            System.out.println(Thread.currentThread().getName() + "\t" + "首次版本号: " + stamp);
            try {
                //暂停下,让t4拿到的版本号和我一样
                TimeUnit.MILLISECONDS.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            asr.compareAndSet(100, 101, asr.getStamp(), asr.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() +
                    "\t" + "2次流水号:" + asr.getStamp() + "\t" + "修改为了:" + asr.getReference());
            asr.compareAndSet(101, 100, asr.getStamp(), asr.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() +
                    "\t" + "3次流水号:" + asr.getStamp() + "\t" + "修改为了:" + asr.getReference());
        }, "t3").start();
        new Thread(() -> {
            //首次版本号
            int stamp = asr.getStamp();
            System.out.println(Thread.currentThread().getName() + "\t" + "首次版本号: " + stamp);
            try {
                //确保线程3发生了ABA问题
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean b = asr.compareAndSet(100, 2022, stamp, stamp + 1);
            System.out.println("修改状态:" + b + "\t" + "现在的值:"+asr.getReference() + "\t" + "现在的版本号:"+asr.getStamp());
        }, "t4").start();
    }


运行结果:
	t3	首次版本号: 1
	t4	首次版本号: 1
	t3	2次流水号:2	修改为了:101
	t3	3次流水号:3	修改为了:100
	修改状态:false	现在的值:100	现在的版本号:3

目录

Total Likes
1
Total Comments
0