Skip to main content

无锁CAS

huhxAbout 4 minjavaConcurrency-ToolkitConcurrency

为了线程间共享数据的安全性,我们引入了锁的机制。不管是synchronized内置锁还是重入锁,在高并发时,对锁的激烈竞争导致的线程等待很大程度上会影响性能。为此,人们想到了一种称为非阻塞同步的方法,这种方式不需要锁。它,就是我们今天要讲的CAS

CAS

CAS全称Compare And Swape,是一种比较并交换的算法。在硬件层面上,大多数处理器架构中,是已经支持原子化的CAS指令的。在JDK 1.5之后,虚拟机就可以使用这个指令了。

CAS包含了3个操作数,它的形式为CAS(V, E, N)。其中V表示要更新的地址,E表示要比较的值,N表示新值。

  • 当V的值等于E时,V的值更新为N,返回的结果为V原有的值E
  • 当V的值不等于E时,说明有其它的线程做了更新,则当前线程就不做V值的更新。返回的结果是V的值

CAS的操作是乐观的,因为它总是希望自己能够成功的执行更新操作。

当多个线程尝试使用CAS更新同一个变量时,只有一个线程能够更新成功,其他线程都将更新失败。而且这些更新失败的线程并不会等待被挂起,而是被告知失败的结果。对于失败后的处理,线程可以自行处理。

线程在竞争CAS失败时不会阻塞,它可以决定是否重试,或者执行一些恢复操作,或者啥都不干直接退出。把这种竞争失败的处理交由给开发人员,这种灵活性就大大减少了与锁相关的活跃性问题了(死锁和优先级反转等等)。

原子操作

内存屏障?原子性是由硬件指令支持的,那内存的可见性呢?

工作原理

Unsafe

优缺点

ABA问题

如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A 值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回 A,那 CAS 操作就会误认为它从来没有被修改过。这个问题被称为 CAS 操作的 "ABA"问题。

ABA 问题的解决思路是在变量前面追加上版本号或者时间戳。JDK 1.5 以后的AtomicStampedReference类就是用来解决 ABA 问题的,其中的compareAndSet()方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

public boolean compareAndSet(V   expectedReference,
                             V   newReference,
                             int expectedStamp,
                             int newStamp) {
    Pair<V> current = pair;
    return
        expectedReference == current.reference &&
        expectedStamp == current.stamp &&
        ((newReference == current.reference &&
          newStamp == current.stamp) ||
         casPair(current, Pair.of(newReference, newStamp)));
}

循环长开销大

CAS 经常会用到自旋操作来进行重试,也就是不成功就一直循环执行直到成功。如果长时间不成功,会给 CPU 带来非常大的执行开销。
如果 JVM 能支持处理器提供的 pause 指令那么效率会有一定的提升,pause 指令有两个作用:

  • 可以延迟流水线执行指令,使 CPU 不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。
  • 可以避免在退出循环的时候因内存顺序冲突而引起 CPU 流水线被清空,从而提高 CPU 的执行效率。

单个共享变量

CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5 开始,提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用AtomicReference类把多个共享变量合并成一个共享变量来操作。

FAQ

总结

参考