深入理解CAS原理!

CAS(compareAndSwap)比较交换,是一种无锁原子算法,映射到操作系统就是一条cmpxchg硬件汇编指令(保证原子性)。

其作用是让CPU将内存值更新为新值,但是有个条件,内存值必须与期望值相同。

并且CAS操作无需用户态与内核态切换,直接在用户态对内存进行读写操作(意味着不会阻塞/线程上下文切换)。

它包含3个参数CAS(V,E,N)V表示待更新的内存值,E表示预期值,N表示新值。

V值等于E值时,才会将V值更新成N值,如果V值和E值不等,不做更新,这就是一次CAS的操作。

image-20231018135610476

CAS如何保证原子性

总线锁定:

CPU使用了总线锁。

总线锁就是使用CPU提供的LOCK#信号,当CPU在总线上输出LOCK#信号时,其他CPU的总线请求将被阻塞。

缓存锁定:

总线锁定方式在锁定期间,会导致大量阻塞,增加系统的性能开销。

所谓缓存锁定是指CPU缓存行进行锁定,当缓存行中的共享变量回写到内存时。

其他CPU会通过总线嗅探机制感知该共享变量是否发生变化,如果发生变化,让自己对应的共享变量缓存行失效。

重新从内存读取最新的数据,缓存锁定是基于缓存一致性机制来实现的。

因为缓存一致性机制会阻止两个以上CPU同时修改同一个共享变量(现代CPU基本都支持和使用缓存锁定机制)。

  • 缓存行是CPU高速缓存存储的最小单位。

CAS的问题

只能保证一个共享变量原子操作:

CAS只能针对一个共享变量使用,如果多个共享变量就只能使用锁了,也可以把多个变量整成一个变量。

  • 也可以利用一个新的类,来整合一组共享变量,利用 AtomicReference 来把这个新对象整体进行 CAS 操作。

自旋时间太长:

当一个线程获取锁时失败,不进行阻塞挂起,而是间隔一段时间再次尝试获取,直到成功为止,这种循环获取的机制被称为自旋。

自旋锁好处:

持有锁的线程在短时间内释放锁,那些等待竞争锁的线程就不需进入阻塞状态(无需线程上下文切换/无需用户态与内核态切换)。

它们只需要等一等(自旋),等到持有锁的线程释放锁之后即可获取,这样就避免了用户态和内核态的切换消耗。

自旋锁坏处:线程在长时间内持有锁,等待竞争锁的线程一直自旋,即CPU一直空转,资源浪费在毫无意义的地方。

ABA问题:

CAS需要检查待更新的内存值有没有被修改,如果没有则更新。

存在这样一种情况,如果一个值原来是A,变成了B,然后又变成了A,在CAS检查的时候会发现没有被修改。

  • 有两个线程,线程1读取到内存值A,线程1时间片用完,切换到线程2,线程2也读取到了内存值A
  • 并把它修改为B值,然后再把B值还原到A值,修改次序是A->B->A
  • 接着线程1恢复运行,它发现内存值还是A,然后执行CAS操作。

要解决ABA问题只要追加版本号即可,每次改变时加1

A —> B —> A,变成1A —> 2B —> 3A,在Java中提供了AtomicStampedRdference可以实现这个方案。