java并发——CAS浅谈

    科技2024-08-10  25

    CAS,Compare And Swap,即比较并交换。Doug lea大神在同步组件中大量使用CAS技术鬼斧神工地实现了Java多线程的并发操作。整个AQS(抽象队列同步)同步组件、Atomic原子类操作等等都是以CAS实现的,甚至ConcurrentHashMap在1.8的版本中也调整为了CAS+Synchronized。可以说CAS是整个JUC的基石。

    cas实现

    CAS(Compare and Swap) 是利用底层硬件平台特性,实现原子性操作的算法,Java 1.5 以后 JUC(java.util.concurrent) 实现主要以此为基础;

    目前的处理器基本都支持CAS,在CAS中有三个参数:内存值V、旧的预期值A、要更新的值B,当且仅当内存值V的值等于旧的预期值A时才会将内存值V的值修改为B,否则什么都不干。

    就以AtomicInteger的addAndGet()方法来做说明,内部调用unsafe的getAndAddInt方法,在getAndAddInt方法中主要是看compareAndSwapInt方法:

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

    该方法为本地方法,有四个参数,分别代表:对象、对象的地址、预期值、修改值。

    原子性实现

    CAS可以保证一次的读-改-写操作是原子操作,通过CPU的cmpxchgl指令的支持,在单处理器上该操作容易实现,但是在多处理器上实现就有点儿复杂了。

    CPU提供了两种方法来实现多处理器的原子操作:总线加锁或者缓存加锁。

    总线加锁:总线加锁就是就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占使用共享内存。但是这种处理方式显得有点儿霸道,不厚道,他把CPU和内存之间的通信锁住了,在锁定期间,其他处理器都不能其他内存地址的数据,其开销有点儿大。所以就有了缓存加锁。缓存加锁:其实针对于上面那种情况我们只需要保证在同一时刻对某个内存地址的操作是原子性的即可。缓存加锁就是缓存在内存区域的数据如果在加锁期间,当它执行锁操作写回内存时,处理器不在输出LOCK#信号,而是修改内部的内存地址,利用缓存一致性协议来保证原子性。缓存一致性机制可以保证同一个内存区域的数据仅能被一个处理器修改,也就是说当CPU1修改缓存行中的i时使用缓存锁定,那么CPU2就不能同时缓存了i的缓存行。

    Unsafe是CAS的核心类

    比如AtomicInteger类的 compareAndSet()方法,调用的就是 Unsafe类的compareAndSwapInt(),Unsafe类是final类型,典型的单例类,里面的方法大多是native方法,调用jvm中c++代码实现,当前的处理器基本都支持CAS,只不过不同的厂家的实现不一样罢了。CAS有三个操作数:内存值V、旧的预期值A、要修改的值B,当且仅当预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做并返回false。由于CAS都是硬件级别的操作,因此效率会高一些。

    valueOffset表示的是变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的原值的。

    CAS缺陷

    CAS虽然高效地解决了原子操作,但是还是存在一些缺陷的,主要表现在三个方面:循环时间太长、只能保证一个共享变量原子操作、ABA问题。

    循环时间太长

    如果CAS一直不成功呢?这种情况绝对有可能发生,如果自旋CAS长时间地不成功,则会给CPU带来非常大的开销。在JUC中有些地方就限制了CAS自旋的次数,例如BlockingQueue的SynchronousQueue。

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

    看了CAS的实现就知道这只能针对一个共享变量,如果是多个共享变量就只能使用锁了,当然如果你有办法把多个变量整成一个变量,利用CAS也不错,例如读写锁中state的高低位

    ABA问题

    CAS需要检查操作值有没有发生改变,如果没有发生改变则更新。但是存在这样一种情况:如果一个值原来是A,变成了B,然后又变成了A,那么在CAS检查的时候会发现没有改变,但是实质上它已经发生了改变,这就是所谓的ABA问题。对于ABA问题其解决方案是加上版本号,即在每个变量都加上一个版本号,每次改变时加1,即A —> B —> A,变成1A —> 2B —> 3A。 java.util.concurrent包为了解决这个问题,提供了一个带有标记的原子引用类”AtomicStampedReference”,通过为引用建立个 Stamp 类似版本号的方式,确保 CAS 操作的正确性。不过目前来说这个类比较”鸡肋”,大部分情况下ABA问题并不会影响程序并发的正确性,如果需要解决ABA问题,使用传统的互斥同步可能回避原子类更加高效。

    参考资料

    【死磕Java并发】----深入分析CAS 聊聊 Java 中的 Unsafe 和 CAS

    Processed: 0.012, SQL: 8