Java并发编程——Java 如何实现原子操作以及实现原子操作的三大问题
·
Java 如何实现原子操作
在 Java 中,原子操作是指在多线程环境下,确保操作不被中断的不可分割操作,主要通过 java.util.concurrent.atomic 包中的类(如 AtomicInteger、AtomicReference 等)来实现。这些类底层依赖于硬件级别的 CAS (Compare-And-Swap) 指令来保证原子性。CAS 是一种乐观锁机制,它通过比较内存值是否与预期值相等,并在相等时更新为新值来实现原子更新。
下面分别回答您的问题。
(1)使用循环 CAS 实现原子操作
CAS 本身只保证单次操作的原子性,但如果在 CAS 执行期间其他线程修改了值,CAS 会失败(返回 false)。为了确保最终成功,需要使用 循环重试 的方式:不断读取当前值、计算新值、尝试 CAS,直到成功为止。这种模式类似于乐观并发控制。
实现原理:
- 使用
sun.misc.Unsafe类(JDK 内部工具类)提供的compareAndSwapInt、compareAndSwapObject等方法,这些方法直接调用 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 高效且无锁,但存在以下三大经典问题:
-
ABA 问题:
- 描述:CAS 只检查值是否相等,而不检查值是否被修改过。线程 A 读取值 X,线程 B 将 X 改为 Y 再改回 X,此时 A 的 CAS 认为值未变而成功更新,但实际值已被篡改(可能导致逻辑错误,如栈弹出错误对象)。
- 示例:假设共享变量为 A,线程1 准备 CAS(A -> C),但线程2 先 A -> B -> A,线程1 误以为未变,直接更新为 C。
- 解决方案:使用版本号或时间戳(如
AtomicStampedReference类,带版本的引用),或避免可变对象。
-
循环时间长 / 高 CPU 占用:
- 描述:高并发下,CAS 失败率高,导致 do-while 循环频繁自旋,浪费 CPU 资源(自旋锁问题)。极端情况下,可能导致“活锁”(线程不断重试但无法前进)。
- 示例:多个线程竞争同一个 AtomicInteger 自增,失败线程会空转 CPU。
- 解决方案:结合自适应策略(如 JDK 的
LongAdder,分段累加减少争用);或在循环中添加短暂休眠/暂停(yield)。
-
只能对单个变量进行原子操作:
- 描述:CAS 一次只能操作一个变量,无法原子地更新多个相关变量(如银行转账需同时扣 A 增 B)。
- 示例:想原子更新
{x=1, y=2}到{x=2, y=3},但 CAS 只能检查一个值。 - 解决方案:使用锁(如
synchronized或ReentrantLock);或组合类如AtomicReference包装整个对象(但仍需小心 ABA);高级如VarHandle(JDK 9+)支持复合操作。
这些问题使 CAS 适合低争用场景,高争用时需结合其他机制(如锁或分段锁)。在实际开发中,优先使用 JDK 提供的原子类,它们已优化了这些问题。
更多推荐


所有评论(0)