Java 如何实现原子操作

在 Java 中,原子操作是指在多线程环境下,确保操作不被中断的不可分割操作,主要通过 java.util.concurrent.atomic 包中的类(如 AtomicIntegerAtomicReference 等)来实现。这些类底层依赖于硬件级别的 CAS (Compare-And-Swap) 指令来保证原子性。CAS 是一种乐观锁机制,它通过比较内存值是否与预期值相等,并在相等时更新为新值来实现原子更新。

下面分别回答您的问题。

(1)使用循环 CAS 实现原子操作

CAS 本身只保证单次操作的原子性,但如果在 CAS 执行期间其他线程修改了值,CAS 会失败(返回 false)。为了确保最终成功,需要使用 循环重试 的方式:不断读取当前值、计算新值、尝试 CAS,直到成功为止。这种模式类似于乐观并发控制。

实现原理

  • 使用 sun.misc.Unsafe 类(JDK 内部工具类)提供的 compareAndSwapIntcompareAndSwapObject 等方法,这些方法直接调用 CPU 的 CAS 指令(如 x86 的 cmpxchg)。
  • 循环结构:do-while 循环,直到 CAS 返回 true。

示例代码:模拟一个简单的原子自增操作(类似于 AtomicInteger.getAndIncrement() 的实现)。

import sun.misc.Unsafe;
import java.lang.reflect.Field;

public class AtomicIntegerDemo {
    private volatile int value;  // 使用 volatile 确保可见性
    private static final Unsafe unsafe = getUnsafe();
    private static final long valueOffset;  // 值字段的内存偏移量

    static {
        try {
            Field valueField = AtomicIntegerDemo.class.getDeclaredField("value");
            valueOffset = unsafe.objectFieldOffset(valueField);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private static Unsafe getUnsafe() {
        return Unsafe.getUnsafe();  // 实际使用需注意权限
    }

    // 原子自增操作:返回旧值
    public final int getAndIncrement() {
        int oldValue;
        int newValue;
        do {
            oldValue = unsafe.getIntVolatile(this, valueOffset);  // 读取当前值(volatile 语义)
            newValue = oldValue + 1;  // 计算新值
        } while (!unsafe.compareAndSwapInt(this, valueOffset, oldValue, newValue));  // CAS 尝试更新
        return oldValue;
    }

    public int getValue() {
        return value;
    }

    public static void main(String[] args) {
        AtomicIntegerDemo ai = new AtomicIntegerDemo();
        // 多线程测试...
        System.out.println("Final value: " + ai.getValue());
    }
}

解释

  • getIntVolatile:原子读取当前值,确保可见性。
  • compareAndSwapInt:比较内存值是否等于 oldValue,如果是则更新为 newValue,返回 true;否则失败,返回 false。
  • 循环:如果失败(其他线程已修改),重新读取并重试,直到成功。
  • 在实际 JDK 中,AtomicInteger 就是这样实现的(源码中可见 do-while 循环)。

这种方式高效,因为大多数情况下 CAS 一次成功;高争用时会自旋重试。

(2)CAS 实现原子操作的三大问题

尽管 CAS 高效且无锁,但存在以下三大经典问题:

  1. ABA 问题

    • 描述:CAS 只检查值是否相等,而不检查值是否被修改过。线程 A 读取值 X,线程 B 将 X 改为 Y 再改回 X,此时 A 的 CAS 认为值未变而成功更新,但实际值已被篡改(可能导致逻辑错误,如栈弹出错误对象)。
    • 示例:假设共享变量为 A,线程1 准备 CAS(A -> C),但线程2 先 A -> B -> A,线程1 误以为未变,直接更新为 C。
    • 解决方案:使用版本号或时间戳(如 AtomicStampedReference 类,带版本的引用),或避免可变对象。
  2. 循环时间长 / 高 CPU 占用

    • 描述:高并发下,CAS 失败率高,导致 do-while 循环频繁自旋,浪费 CPU 资源(自旋锁问题)。极端情况下,可能导致“活锁”(线程不断重试但无法前进)。
    • 示例:多个线程竞争同一个 AtomicInteger 自增,失败线程会空转 CPU。
    • 解决方案:结合自适应策略(如 JDK 的 LongAdder,分段累加减少争用);或在循环中添加短暂休眠/暂停(yield)。
  3. 只能对单个变量进行原子操作

    • 描述:CAS 一次只能操作一个变量,无法原子地更新多个相关变量(如银行转账需同时扣 A 增 B)。
    • 示例:想原子更新 {x=1, y=2}{x=2, y=3},但 CAS 只能检查一个值。
    • 解决方案:使用锁(如 synchronizedReentrantLock);或组合类如 AtomicReference 包装整个对象(但仍需小心 ABA);高级如 VarHandle(JDK 9+)支持复合操作。

这些问题使 CAS 适合低争用场景,高争用时需结合其他机制(如锁或分段锁)。在实际开发中,优先使用 JDK 提供的原子类,它们已优化了这些问题。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐