CAS是Java无锁编程的基石。ABA问题考你是否理解CAS的根本弱点,和知道AtomicStampedReference这类针对性工具。
CAS是CPU原子指令——compare-and-swap:如果地址X当前值等于期望E就原子地写入N并返回成功,否则不动返回失败。
Java通过Unsafe(或9+的VarHandle)暴露。AtomicInteger、AtomicLong、ConcurrentHashMap都基于它。典型用法是while循环:读当前、算新值、CAS;失败说明别人改过了,重试。
ABA问题:T1读到A准备CAS;T2把A改成B再改回A;T1的CAS看到A成功,但中间状态变过了,T1的视图已过期。
具体场景:无锁栈。T1想把head从A改成A.next。中间T2 pop A、pop B、又push A(内存复用),head又是A但链表完全不一样了。T1的CAS成功但把head设成了一个过期的next指针,栈就乱了。
解法:AtomicStampedReference给值配一个单调递增的stamp,CAS同时比对值和stamp。中间任何变化都会bump stamp,ABA就能被检测到。
CAS是硬件原子指令——compare-and-swap,语义是"地址X的当前值如果等于期望E,原子地替换成N返回成功;否则不动返回失败"。是非阻塞并发的基石。Java通过Unsafe或VarHandle访问,AtomicInteger、AtomicLong、ConcurrentHashMap的桶CAS都是它。典型用法是重试循环:读当前值、算新值、CAS;有人在中间改了就CAS失败,重试。ABA问题是CAS的根本弱点。线程T1读到值A准备CAS,还没执行时T2把A改成B再改回A。T1终于执行CAS,看到A成功——但底层状态经过了中间值,现在的A可能语义上和T1当初看到的A不一样了。对原子整数计数器这种场景几乎没影响,对引用的无锁数据结构就是灾难。经典例子:AtomicReference做head的无锁栈。T1读head=A,计划把head从A改成A.next。这时T2 pop A(head变B)、pop B(head变null)、新分配一个节点(正好复用了A的地址)push进来(head又是A)。T1的CAS看到A成功,把head设成A.next——但A.next是原始视角里的B,现在已经被pop、被free了。栈的head指向了无效内存。解法是AtomicStampedReference:包一对(值+int stamp)。每次set都bump stamp,compareAndSet同时比值和stamp。任何中间写都会改stamp,即使值回到A也会被检测到,CAS失败重试。AtomicMarkableReference类似,包boolean mark,适合"删除"标记。实战里原子计数器不用管这个,引用类的无锁结构必须用。
ABA一定要举具体的栈或队列例子——光说"有个问题"像背书。
能点名AtomicStampedReference是合适的具体度。
区分ABA敏感场景(引用/内存)和不敏感场景(原子计数器)。
忘了AtomicStampedReference,即答侠可以实时提示。
低竞争下CAS快很多(无系统调用无线程park)。高竞争时重试堆积锁反而赢,LongAdder用分片解决这个问题。
会——CAS独占cache line,同一个变量高竞争会在多核间cache line ping-pong严重降速。
某些架构(ARM LL/SC)下CAS会因为上下文切换或缓存事件虚假失败——必须循环重试,标准while(!CAS)模式已经覆盖了。