什么是CAS操作

CAS操作

之前说在java.util.concurrent.atomic包下提供的原子操作类底层使用的是CAS,那么什么是CAS呢,CAS的全称为Compare And Swap,比较并替换,CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B

在进行变量更新时,需要对比预期值A和内存地址V中的实际值,如果两者相同,才会将V对应的的值改为B

AtomicInteger为例

java 复制代码
// unsafe提供了硬件级别的原子操作
private static final Unsafe unsafe = Unsafe.getUnsafe();
// 存储value在AtomicInteger中的偏移量
private static final long valueOffset;
// 存储AtomicInteger的int值,该属性需要借助volatile关键字保证其在线程间是可见的
private volatile int value;

static {
  try {
    // valueOffset表示的是AtomicInteger对象value成员变量在内存中的偏移量,可以当做是value变量的内存地址
    valueOffset = unsafe.objectFieldOffset
      (AtomicInteger.class.getDeclaredField("value"));
  } catch (Exception ex) { throw new Error(ex); }
}

public final boolean compareAndSet(int expect, int update) {
  // this为当前对象,valueOffset参数代表了V对象中的变量的偏移量,expect参数代表了A变量预期值,update参数代表了B新的值
  // 如果对象this中内存偏移量为valueOffset的变量值为expect,则使用新的值update替换旧的值expect
  return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

Unsafe类

CAS的底层使用的是Unsafe类,Unsafe类中的方法都是native方法,看下提供了哪些方法

java 复制代码
// 返回指定的变量在所属类中的内存偏移地址,该偏移地址仅仅在该Unsafe函数中指定字段时使用
public native long objectFieldOffset(Field field);
// 获取数组中第一个元素的地址
public native int arrayBaseOffset(Class<?> arrayClass);
// 获取数组中一个元素占用的字节
public native int arrayIndexScale(Class<?> arrayClass);
// 比较对象obj中偏移量为offset的变量值是否与except相等,相等则使用update替换旧的except
public final native boolean compareAndSwapInt(Object obj, long offset, int except, int update);
// 获取obj对象中偏移量为offset的变量对应volatile语义的值
public native Object getObjectVolatile(Object obj, long offset);
// 设置obj对象中offset偏移的类型为Object的field的值为value
public native void putObjectVolatile(Object obj, long offset, Object value);
// 阻塞当前线程,isAbsolute等于false且time等于0表示一直阻塞;time大于0表示等待指定的time后阻塞线程会被唤醒,这个time为所休眠的时间;如果isAbsolute等于true,且time大于0,表示阻塞的线程到指定的时间点后会被唤醒,这个time为毫秒级的时间戳
// 如果其他线程调用了当前阻塞线程的interrupt方法而中断了当前线程时,或者其他线程调用了unPark方法并且把当前线程作为参数时,当前线程都会返回
public native void park(boolean isAbsolute, long time);
// 唤醒调用park后阻塞的线程
public native void unpark(Object thread);

Unsafe类不可以直接使用,因为Unsafe会判断当前的类加载器是不是Bootstrap类加载器,如果不是的话,会抛出异常SecurityException

java 复制代码
public static Unsafe getUnsafe() {
    Class var0 = Reflection.getCallerClass();
    if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
        throw new SecurityException("Unsafe");
    } else {
        return theUnsafe;
    }
}

既然不能直接使用,那么只能使用反射来使用Unsafe类了

java 复制代码
public class TestUnsafe {
    static final Unsafe unsafe;

    static final long valueOffset;

    private volatile int value = 0;

    static {
        try {
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);

            unsafe = (Unsafe) theUnsafe.get(null);

            valueOffset = unsafe.objectFieldOffset(TestUnsafe.class.getDeclaredField("value"));
        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
            throw new RuntimeException("");
        }

    }

    public static void main(String[] args) {
        TestUnsafe testUnsafe = new TestUnsafe();

        unsafe.getAndAddInt(testUnsafe, valueOffset, 1);
        System.out.println(testUnsafe.value);
    }
}

CAS的缺陷

虽然CAS采用的无锁操作来提供性能,但是CAS并不是完美的,存在了很多的不足

  • CPU开销大,因为在高并发的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,一直循环下去,会给CPU带来很大的压力
  • 不能保证代码块的原子性,CAS只能保证一个变量的原子性操作,而不能保证整个代码块的原子性
  • ABA问题,由于CAS对比的是两个最终值,所以可能会导致中间过程中值变化无法感知,变量的值从A变成B,然后再从B变成A,构成了环形转换

ABA问题的解决

解决这个问题很简单,ABA的本质就是无法感知中间过程,那么加一个版本号就可以了,每次版本号递增1,在比较时,不仅要比较内存地址V和旧的预期值A,还要比较一下版本号

在JDK中AtomicStampedReference类给每个变量都配备了一个时间戳stamp,从而避免了ABA问题的产生

java 复制代码
int stamp = 1;
// 使用stamp来作为初始时间戳,为数据增加一个版本号,在修改数据的时候除了提供旧值之外,还要提供旧的版本号
AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(0,stamp);
// 预期值,更新后的值,预期版本,更新后的版本 
stampedReference.compareAndSet(0,1,stamp,stamp+1);

zhhll.icu/2021/多线程/基础...

本文由mdnice多平台发布

相关推荐
微风粼粼17 分钟前
程序员在线接单
java·jvm·后端·python·eclipse·tomcat·dubbo
缘来是庄21 分钟前
设计模式之中介者模式
java·设计模式·中介者模式
rebel1 小时前
若依框架整合 CXF 实现 WebService 改造流程(后端)
java·后端
代码的余温2 小时前
5种高效解决Maven依赖冲突的方法
java·maven
慕y2742 小时前
Java学习第十六部分——JUnit框架
java·开发语言·学习
paishishaba2 小时前
Maven
java·maven
张人玉3 小时前
C# 常量与变量
java·算法·c#
Java技术小馆3 小时前
GitDiagram如何让你的GitHub项目可视化
java·后端·面试
Codebee3 小时前
“自举开发“范式:OneCode如何用低代码重构自身工具链
java·人工智能·架构
程序无bug3 小时前
手写Spring框架
java·后端