概述

  • CAS是Compare And Swap的简称,即:比较并交换
  • Java中常见的是Atomic相关类使用了CAS,例如:AtomicInteger、AtomicBoolean等等
  • 实现CAS的底层用的是Unsafe操作类

先看个小例子

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

    atomicInteger.compareAndSet(5, 100);
    System.out.println(atomicInteger.get());
    
    atomicInteger.compareAndSet(5, 200);
    System.out.println(atomicInteger.get());
}

输出:

100
100
  • compareAndSet(int expect, int update):将atomicInteger数值与expect比较,相等就把update赋给atomicInteger,否则什么都不做。
  • 所以,上面的例子只有第一次是修改成功的,第二次修改失败,值仍然为100.

compareAndSet 源码:调用了unsafe.compareAndSwapInt

public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
  • this:当前AtomicInteger对象
  • valueOffset:AtomicInteger的value字段在内存中的偏移量
  • expect:期望值,需要比较的值
  • update:如果value==expect,需要更新替换(Swap)的数据

Unsafe

  • Unsae是CAS的核心类,由于Java无法直接访问底层系统,需要通过本地native方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。
  • Unsafe类存在于sun.misc包中,其内部方法可以像C语言的指针一样直接操作内存,因此Java中CAS的操作执行依赖于Unsafe类的方法。

现在,再来看看上面的compareAndSet,内部使用的unsafe.compareAndSwapInt,其实底层就一句native代码。但是,最终执行是由一系列原子的、不可穿插、不可中断的指令组成,以此来实现CAS原子性操作

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

例子2:getAndIncrement

AtomicInteger还有个常用的方法:getAndIncrement。作用就是把当前数值+1,跟i++效果一样。但是getAndIncrement是线程安全的,而i++不是线程安全。

getAndIncrement 源码解析,调用的是 unsafe.getAndAddInt

    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }

再来看下 unsafe.getAndAddInt 源码

    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;
    }
  1. 根据 AtomicInteger 的 valueOffset,即内存偏移量,获取内存中的值 var5
  2. 执行 compareAndSwapInt,把 var5 再次和内存的值比较一次,相同就执行 var5 + var4(这里var4=1)
  3. 如果上一步比较结果不同,则重新回到第1步,重新执行
  4. 返回 var5

CAS缺点

  • getAndAddInt使用的是do…while,执行失败会一直反复尝试。如果长时间一直不成功,会给CPU带来较大的开销。
  • 只能保证一个共享变量的原子操作,如果需要保证多个变量的原子操作,可以采用锁,或者AtomicReference操作对象
  • 引出来ABA问题

ABA问题

ABA 问题阐述

  1. 假设主内存有变量A=0,现在2个线程都准备执行compareAndSet(0, 10)
  2. 由于线程2的执行速度较快,把A改成10,然后又改成了20,最后又改成10
  3. 此时线程1也正准备执行,因为主内存A=10,线程1中也是A=10,就以为当前变量A没人操作过,所以线程1的compareAndSet也执行成功了
  4. 表面上看似乎也没什么问题,但是这个现象,在特定的业务条件下可能就会导致Bug

ABA 问题解决

使用AtomicStampedReference可以解决ABA问题,因为这个类携带版本号功能。除了比较当前值和预期值,还会比较当前版本号和预期版本号。如果版本号不一致,则同样更新失败。

public class AtomicStampedReferenceApp {
    public static void main(String[] args) {
    	//初始化AtomicStampedReference对象,值为5,版本号为1
        AtomicStampedReference<Integer> atomic = new AtomicStampedReference<Integer>(5, 1);

        new Thread(() -> {
            int stamp = atomic.getStamp();
            System.out.println(Thread.currentThread().getName() + "初始版本号:" + stamp);
            //Sleep 1s
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //先从5改成10
            atomic.compareAndSet(5, 10, atomic.getStamp(), atomic.getStamp()+1);
            //再从10改成5
            atomic.compareAndSet(10, 5, atomic.getStamp(), atomic.getStamp()+1);
            //输出结果,这时值为5,版本号为3
            System.out.println(Thread.currentThread().getName() + "当前值:" + atomic.getReference() + ",当前版本号:" + atomic.getStamp());
        }, "thread-1").start();

        new Thread(() -> {
            int stamp = atomic.getStamp();
            System.out.println(Thread.currentThread().getName() + "初始版本号:" + stamp);
            //Sleep 3s,保证thread-1先执行完毕,到时版本号会更高
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //预期将值从5改成100,预期版本号是1
            boolean result = atomic.compareAndSet(5, 100, stamp, stamp+1);
            System.out.println(Thread.currentThread().getName() + "修改成功:" + result + ",当前值:" + atomic.getReference() + ",当前版本号:" + atomic.getStamp());
        }, "thread-2").start();
    }
}

结果如下,thread-2修改失败,因为当前版本号已经变成3了,比预期的1还高

thread-1初始版本号:1
thread-2初始版本号:1
thread-1当前值:5,当前版本号:3
thread-2修改成功:false,当前值:5,当前版本号:3
posted on 2020-09-17 16:48  风停了,雨来了  阅读(499)  评论(0编辑  收藏  举报