volatile与CAS:无锁编程的底层奥秘
前言
volatile和CAS(Compare-And-Swap)是Java并发编程中最基础也最重要的概念之一。它们是Java内存模型(JMM)的基石,也是理解JUC并发包的钥匙。很多开发者知道volatile保证可见性、CAS是原子操作,但对其底层原理、实现机制以及适用场景往往一知半解。本文将从CPU指令级别深入剖析这两个概念。
一、并发编程的三大问题
1.1 可见性问题
/**
* 可见性问题演示
*/
public class VisibilityProblem {
private static boolean flag = true; // 非volatile
public static void main(String[] args) throws InterruptedException {
// 线程1:不断检查flag
Thread t1 = new Thread(() -> {
int count = 0;
while (flag) {
count++;
// 问题:线程1可能永远看不到线程2对flag的修改
}
System.out.println("线程1退出,count=" + count);
});
// 线程2:修改flag
Thread t2 = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {}
flag = false; // 修改flag
System.out.println("线程2修改flag为false");
});
t1.start();
t2.start();
t1.join();
t2.join();
}
}
/**
* 问题分析:
*
* 1. 线程1在CPU Core1上运行,将flag加载到Core1的缓存中
* 2. 线程2在CPU Core2上运行,修改了flag(写到Core2的缓存或主内存)
* 3. 由于CPU缓存一致性协议的实现问题,Core1可能看不到这个修改
* 4. 线程1陷入死循环(不可见)
*/
1.2 有序性问题
/**
* 有序性问题(指令重排序)
*/
public class OrderingProblem {
private int a = 0;
private int b = 0;
private int x = 0;
private int y = 0;
public void reorderScenario() throws InterruptedException {
int iterations = 0;
while (true) {
iterations++;
a = 0;
b = 0;
x = 0;
y = 0;
Thread t1 = new Thread(() -> {
a = 1; // 操作1
x = b; // 操作2
});
Thread t2 = new Thread(() -> {
b = 1; // 操作3
y = a; // 操作4
});
t1.start();
t2.start();
t1.join();
t2.join();
// 正常情况下,可能的结果:(1,1), (1,0), (0,1)
// 但不可能是(0,0)吗?让我们测试
if (x == 0 && y == 0) {
System.out.println("发现重排序导致的结果: x=0, y=0");
System.out.println("迭代次数: " + iterations);
break;
}
}
}
}
/**
* 问题分析:
*
* 在单线程环境下,编译器/CPU可能对指令重排序以优化性能
* 线程1的指令顺序可能被重排为:
* x = b; // 先读取b
* a = 1; // 再写入a
*
* 这种重排序在单线程下是安全的
* 但在多线程环境下可能导致违反直觉的结果
*/
1.3 原子性问题
/**
* 原子性问题演示
*/
public class AtomicityProblem {
private static int counter = 0; // 非volatile,非原子
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[1000];
for (int i = 0; i < 1000; i++) {
threads[i] = new Thread(() -> {
counter++; // 非原子操作
});
threads[i].start();
}
for (Thread t : threads) {
t.join();
}
// 期望结果:1000
// 实际结果:可能是956、987等,小于1000
System.out.println("最终counter值: " + counter);
}
}
/**
* 问题分析:
*
* counter++ 看似一个操作,实际上包含三个步骤:
* 1. LOAD :从内存读取counter到CPU寄存器
* 2. ADD :CPU寄存器中+1
* 3. STORE :将结果写回内存
*
* 多线程执行时:
* 线程1: LOAD counter(0) → ADD → STORE counter=1 ✓
* 线程2: LOAD counter(0) → ADD → STORE counter=1 ✓
* 以上假设执行顺利,但...
*
* 线程1: LOAD counter(0)
* 线程2: LOAD counter(0) ← 两个线程同时读取
* 线程1: ADD → STORE counter=1
* 线程2: ADD → STORE counter=1 ← 覆盖了线程1的结果
* 结果:counter=1,丢失了一次更新!
*/
二、volatile详解
2.1 volatile的作用
/**
* volatile保证的两大特性:
* 1. 可见性:写操作对其他线程立即可见
* 2. 有序性:禁止指令重排序
*/
public class VolatileDemo {
// 使用volatile修饰,解决可见性问题
private volatile static boolean flag = true;
// volatile保证有序性的场景
private volatile int x = 0;
private volatile int y = 0;
public void orderedWrite() {
// volatile写之前的指令不能重排到volatile写之后
a = 1; // 普通写
x = 1; // volatile写 - 屏障
// x写之前的指令不能重排到x写之后
// volatile读之后的指令不能重排到volatile读之前
int r1 = x; // volatile读 - 屏障
int r2 = a; // 普通读
// r2读不能重排到r1读之前
}
}
2.2 volatile底层实现:内存屏障
/**
* JMM定义的4种内存屏障:
*
* StoreLoad Barriers:
* - 最重的屏障,兼顾Store和Load
* - 防止之前的写操作与之后的读操作重排序
* - 在x86架构下会生成MFENCE指令
*
* 各种volatile操作的屏障:
*
* volatile读:
* 在读操作后插入LoadLoad屏障和LoadStore屏障
* ┌─────────────────────────────┐
* │ Load │
* │ LoadLoad Barrier │
* │ LoadStore Barrier │
* └─────────────────────────────┘
*
* volatile写:
* 在写操作前插入StoreStore屏障
* 在写操作后插入StoreLoad屏障
* ┌─────────────────────────────┐
* │ StoreStore Barrier │
* │ Store (volatile write) │
* │ StoreLoad Barrier │
* └─────────────────────────────┘
*/
2.3 volatile在JMM中的语义
┌─────────────────────────────────────────────────────────────────────┐
│ volatile 读写与内存屏障 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ volatile写操作: │
│ │
│ Thread A (写入) Thread B (读取) │
│ │ │ │
│ │ │ │
│ ▼ │ │
│ ┌─────────┐ │ │
│ │ 普通写1 │ │ │
│ └────┬────┘ │ │
│ │ │ │
│ │ StoreStore Barrier │ │
│ ├───────────────────────────────┤ │
│ │ │ │
│ ▼ │ │
│ ┌─────────────────┐ │ │
│ │ volatile write │ ─────────────────┼───► 可见(刷新到主内存) │
│ └────────┬────────┘ │ │
│ │ │ │
│ │ StoreLoad Barrier │ │
│ ├────────────────────────────┤ │
│ │ ▼ │
│ │ ┌─────────────────┐ │
│ │ │ LoadStore Barrier│ │
│ │ └────────┬────────┘ │
│ │ │ │
│ │ ▼ │
│ │ ┌─────────────────┐ │
│ │ │ volatile read │ ◄── 强制从主内存读取 │
│ │ └────────┬────────┘ │
│ │ │ │
│ │ ▼ │
│ │ ┌─────────────────┐ │
│ │ │ 普通读 │ │
│ │ └─────────────────┘ │
│ │ │ │
│ │ ┌─────────────────┐ │
│ │ │ LoadLoad Barrier │ │
│ │ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
2.4 volatile的使用场景
/**
* volatile的适用场景:
*
* 1. 状态标志
* 2. 一次性安全发布(双重检查锁定的单例)
* 3. 观察者模式中的Observable对象
* 4. 替代锁的简单计数
*/
public class VolatileUsage {
// 场景1:状态标志
private volatile boolean running = true;
public void stop() {
running = false; // 线程立即看到停止信号
}
// 场景2:一次性发布
private volatile Object instance;
public Object getInstance() {
if (instance == null) { // 第一次检查
synchronized (this) {
if (instance == null) { // 第二次检查
instance = new Object(); // 创建对象
}
}
}
return instance;
}
// 场景3:计数器(需要复合操作的场景不能用volatile)
private volatile long counter = 0;
// ❌ 错误:counter++不是原子操作
public void incrementWrong() {
counter++; // 不安全!
}
// ✅ 正确:使用AtomicLong
private AtomicLong safeCounter = new AtomicLong(0);
public void incrementCorrect() {
safeCounter.incrementAndGet(); // 原子操作
}
}
三、CAS详解
3.1 CAS原理
/**
* CAS (Compare-And-Swap) 原理:
*
* CAS(V, A, B) 操作包含三个参数:
* - V:内存地址
* - A:期望值
* - B:新值
*
* 操作语义:
* 如果V的值等于A,则将B写入V
* 否则,不做任何操作
* 返回V的当前值
*
* 这是一个原子操作,由CPU硬件保证
*/
// Java中CAS的使用
public class CASDemo {
private AtomicInteger value = new AtomicInteger(0);
public void increment() {
// getAndIncrement内部使用CAS
value.getAndIncrement();
}
public boolean compareAndSwap(int expected, int newValue) {
// CAS操作:只有当前值等于expected时,才设置为newValue
return value.compareAndSet(expected, newValue);
}
}
/**
* CAS在JVM中的实现:
*
* JVM会调用Unsafe类的CAS方法
* Unsafe类封装了CPU的CAS指令
*
* 例如 x86架构下的 CMPXCHG 指令
*/
3.2 CPU指令级别的CAS
┌─────────────────────────────────────────────────────────────────────┐
│ CAS 硬件实现 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ x86架构使用 CMPXCHG (Compare and Exchange) 指令: │
│ │
│ CMPXCHG [mem], reg │
│ │
│ 语义: │
│ IF (EAX == [mem]) THEN // 比较EAX寄存器和内存值 │
│ ZF = 1; // 设置零标志 │
│ [mem] = reg; // 交换 │
│ ELSE // 不相等 │
│ ZF = 0; // 清零标志 │
│ EAX = [mem]; // 更新EAX为内存值 │
│ FI │
│ │
│ 多核情况下的CAS使用LOCK前缀: │
│ LOCK CMPXCHG [mem], reg │
│ │
│ LOCK前缀的作用: │
│ 1. 在修改前锁定CPU缓存行(Cache Lock) │
│ 2. 强制写操作对其他CPU可见(Store Buffer失效) │
│ 3. 确保原子性 │
│ │
└─────────────────────────────────────────────────────────────────────┘
3.3 CAS的ABA问题
/**
* ABA问题:
*
* 线程1读取内存值A
* 线程2将A改为B,再改回A
* 线程1进行CAS操作,发现值还是A,认为没有被修改过
* 但实际上已经被修改过了!
*/
public class ABAProblem {
public void demonstrateABA() {
AtomicInteger value = new AtomicInteger(1);
System.out.println("初始值: " + value.get());
// 线程2:修改为2,再改回1
Thread t2 = new Thread(() -> {
value.set(2);
value.set(1);
System.out.println("线程2修改完成: " + value.get());
});
// 线程1:期望1变为3,但实际是1
Thread t1 = new Thread(() -> {
try {
Thread.sleep(100);
} catch (InterruptedException e) {}
boolean success = value.compareAndSet(1, 3);
System.out.println("线程1 CAS结果: " + success +
", 值: " + value.get());
});
t2.start();
t1.start();
}
}
/**
* ABA问题的解决方案:
* 1. AtomicStampedReference:带版本号的引用
* 2. AtomicMarkableReference:带标记位的引用
*/
// 使用AtomicStampedReference解决ABA问题
public class ABASolution {
private AtomicStampedReference<Integer> value =
new AtomicStampedReference<>(1, 0);
public void correctCAS() {
int stamp = value.getStamp();
Integer current = value.getReference();
// 只有值和版本号都匹配时才更新
boolean success = value.compareAndSet(
current, // 期望值
current + 1, // 新值
stamp, // 期望版本号
stamp + 1 // 新版本号
);
}
}
3.4 CAS的缺点
/**
* CAS的缺点:
*
* 1. ABA问题(可使用AtomicStampedReference解决)
* 2. 高竞争下大量失败重试,浪费CPU
* 3. 只能保证单个变量的原子性
* 4. 复杂数据结构需要手工实现
*/
public class CASLimitations {
// 缺点1:竞争激烈时的性能问题
public void contentionDemo() {
AtomicInteger counter = new AtomicInteger(0);
// 1000个线程同时竞争
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
// 激烈竞争下,大量CAS失败
while (!counter.compareAndSet(
counter.get(),
counter.get() + 1)) {
// 自旋重试,浪费CPU
Thread.yield(); // 让出CPU
}
}).start();
}
}
// 缺点2:无法保证复合操作的原子性
public void compoundOperation() {
AtomicInteger balance = new AtomicInteger(100);
// ❌ 这不是原子操作!
// 可能导致负数余额
if (balance.get() >= 100) { // 检查
try {
Thread.sleep(1); // 检查和扣款之间可能被其他线程修改
} catch (InterruptedException e) {}
balance.set(balance.get() - 100); // 扣款
}
// ✅ 使用AtomicInteger的正确方法
// 循环直到成功
while (true) {
int current = balance.get();
if (current >= 100) {
if (balance.compareAndSet(current, current - 100)) {
break; // 成功
}
// 失败,重试
} else {
break; // 余额不足
}
}
}
}
四、无锁编程实战
4.1 使用Atomic原子类
import java.util.concurrent.atomic.*;
/**
* Java提供的原子类:
*
* 基本类型:
* AtomicInteger, AtomicLong, AtomicBoolean
*
* 引用类型:
* AtomicReference, AtomicMarkableReference, AtomicStampedReference
*
* 数组:
* AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray
*
* 属性更新器:
* AtomicIntegerFieldUpdater, AtomicLongFieldUpdater,
* AtomicReferenceFieldUpdater
*/
public class AtomicClassesDemo {
// 基本类型原子类
private AtomicInteger atomicInt = new AtomicInteger(0);
// 引用类型原子类
private AtomicReference<User> userRef = new AtomicReference<>();
// 数组原子类
private AtomicIntegerArray intArray = new AtomicIntegerArray(10);
public void atomicDemo() {
// AtomicInteger常用方法
atomicInt.getAndIncrement(); // i++
atomicInt.incrementAndGet(); // ++i
atomicInt.getAndDecrement(); // i--
atomicInt.decrementAndGet(); // --i
atomicInt.getAndAdd(5); // i += 5
atomicInt.addAndGet(5); // +=5后返回
atomicInt.compareAndSet(10, 20); // CAS: if(i==10) i=20
// lambda更新(JDK 8+)
atomicInt.updateAndGet(x -> x + 1); // x -> x + 1
atomicInt.accumulateAndGet(5, Integer::sum); // x + 5
// 引用类型
userRef.compareAndSet(null, new User("Tom"));
}
}
4.2 实现无锁线程安全单例
/**
* 双重检查锁定单例 vs 无锁单例
*/
public class SingletonPatterns {
// 双重检查锁定(需要volatile)
public static class DoubleCheckedSingleton {
private static volatile DoubleCheckedSingleton instance;
public static DoubleCheckedSingleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (DoubleCheckedSingleton.class) {
if (instance == null) { // 第二次检查
instance = new DoubleCheckedSingleton();
// new操作分解为:
// 1. 分配内存
// 2. 调用构造函数
// 3. 赋值给instance
// volatile防止指令重排序,确保2在3之前完成
}
}
}
return instance;
}
}
// 无锁单例(基于CAS)
public static class CASBasedSingleton {
private static final AtomicReference<CASBasedSingleton> instance =
new AtomicReference<>();
public static CASBasedSingleton getInstance() {
CASBasedSingleton current = instance.get();
if (current == null) {
current = new CASBasedSingleton();
if (!instance.compareAndSet(null, current)) {
// 竞争失败,有其他线程已经创建
current = instance.get();
}
}
return current;
}
}
// 静态内部类(利用类加载机制,推荐)
public static class StaticInnerSingleton {
private static class SingletonHolder {
static final StaticInnerSingleton instance =
new StaticInnerSingleton();
}
public static StaticInnerSingleton getInstance() {
return SingletonHolder.instance;
}
}
// 枚举单例(最安全,推荐)
public enum EnumSingleton {
INSTANCE;
public void doSomething() {
System.out.println("EnumSingleton doing something");
}
}
}
4.3 无锁队列实现
import java.util.concurrent.atomic.*;
/**
* 基于CAS的无锁队列(简化版)
*/
public class LockFreeQueue<E> {
private static class Node<E> {
volatile E item;
volatile Node<E> next;
Node(E item) {
this.item = item;
}
}
private final AtomicReference<Node<E>> head;
private final AtomicReference<Node<E>> tail;
public LockFreeQueue() {
Node<E> dummy = new Node<>(null);
head = new AtomicReference<>(dummy);
tail = new AtomicReference<>(dummy);
}
/**
* 入队操作
* 使用CAS确保只有一个线程能成功修改tail
*/
public void enqueue(E item) {
Node<E> newNode = new Node<>(item);
newNode.next.set(null); // 新节点next初始化为null
while (true) {
Node<E> currentTail = tail.get();
Node<E> tailNext = currentTail.next.get();
// 再次检查tail是否还是尾部
if (currentTail == tail.get()) {
if (tailNext != null) {
// 队列正在更新tail,帮助推进tail
tail.compareAndSet(currentTail, tailNext);
} else {
// 尝试将新节点连接到尾部
if (currentTail.next.compareAndSet(null, newNode)) {
// 成功,尝试推进tail
tail.compareAndSet(currentTail, newNode);
return;
}
}
}
}
}
/**
* 出队操作
* 使用CAS确保只有一个线程能成功修改head
*/
public E dequeue() {
while (true) {
Node<E> currentHead = head.get();
Node<E> currentTail = tail.get();
Node<E> headNext = currentHead.next.get();
if (currentHead == head.get()) {
if (currentHead == currentTail) {
// 队列为空
if (headNext == null) {
return null;
}
// tail落后,帮助推进
tail.compareAndSet(currentTail, headNext);
} else {
// 队列非空
E item = headNext.item;
// 尝试更新head
if (head.compareAndSet(currentHead, headNext)) {
return item;
}
// 失败,重试
}
}
}
}
}
五、性能对比
5.1 synchronized vs volatile vs CAS
/**
* 性能对比:
*
* synchronized:
* - 可重入锁
* - 阻塞线程,线程切换开销大
* - 适用场景:写多、竞争激烈、需要保证复合操作原子性
*
* volatile:
* - 非阻塞
* - 无线程切换开销
* - 适用场景:状态标志、一次性发布、简单读写
*
* CAS (Atomic*):
* - 非阻塞
* - 失败重试,CPU开销
* - 适用场景:计数器、简单复合操作、高读低写
*/
public class PerformanceComparison {
private int counter1 = 0; // 普通变量
private volatile int counter2 = 0; // volatile
private AtomicInteger counter3 = new AtomicInteger(0); // CAS
// synchronized计数器
private int counter4 = 0;
public synchronized void incrementSync() {
counter4++;
}
public void benchmark(int iterations) throws InterruptedException {
// 1. 普通变量(无同步,多个线程各跑各的)
Thread[] threads1 = new Thread[4];
for (int i = 0; i < 4; i++) {
threads1[i] = new Thread(() -> {
for (int j = 0; j < iterations; j++) {
counter1++; // 竞态条件,结果不可靠
}
});
}
// 2. volatile(可见性保证,但++非原子)
Thread[] threads2 = new Thread[4];
for (int i = 0; i < 4; i++) {
threads2[i] = new Thread(() -> {
for (int j = 0; j < iterations; j++) {
counter2++; // 结果不可靠
}
});
}
// 3. AtomicInteger(CAS保证原子性)
Thread[] threads3 = new Thread[4];
for (int i = 0; i < 4; i++) {
threads3[i] = new Thread(() -> {
for (int j = 0; j < iterations; j++) {
counter3.incrementAndGet(); // 原子操作
}
});
}
// 4. synchronized
Thread[] threads4 = new Thread[4];
for (int i = 0; i < 4; i++) {
threads4[i] = new Thread(() -> {
for (int j = 0; j < iterations; j++) {
incrementSync(); // 同步操作
}
});
}
// 执行并计时...
}
}
/**
* 总结:
*
* | 方案 | 可见性 | 原子性 | 性能 | 适用场景 |
* |--------------|--------|--------|------|------------------------|
* | 普通变量 | × | × | 最快 | 单线程 |
* | volatile | ✓ | × | 快 | 状态标志 |
* | Atomic* | ✓ | ✓ | 快 | 计数器、简单复合操作 |
* | synchronized | ✓ | ✓ | 中 | 复杂复合操作 |
*/
总结
volatile和CAS是Java并发编程的两大基石:
- volatile的作用:
- 保证可见性:写操作立即刷新到主内存,读操作立即从主内存读取
- 保证有序性:通过内存屏障防止指令重排序
-
不保证原子性:复合操作(如i++)仍需要其他同步手段
-
CAS的原理:
- 硬件级别的原子指令(x86的CMPXCHG)
- 三个操作在一个原子指令中完成
-
解决ABA问题需要使用带版本号的引用类型
-
选择策略:
- 简单状态标志 → volatile
- 计数器、简单复合操作 → Atomic*类
-
复杂复合操作 → synchronized或Lock
-
注意事项:
- volatile不能替代锁
- CAS在高竞争下有性能问题
- ABA问题在特定场景下可能导致bug
- 无锁编程需要仔细验证正确性
理解这两者的底层原理,是深入学习Java并发包、设计高性能并发系统的基础。

880

被折叠的 条评论
为什么被折叠?



