Java并发-从CPU角度来看并发问题的本质
一个令人困惑的并发问题
先看这段模拟并发问题的代码:
java
public class ConcurrencyExample {
private static int counter = 0;
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[1000];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
counter++; // 非原子操作
}
});
threads[i].start();
}
for (Thread t : threads) { t.join(); }
System.out.println("预期值: 1000000, 实际值: " + counter);
}
}
运行结果总是小于1000000,这个现象揭示了并发问题的本质。要理解这个问题,我们需要深入CPU架构层面。
CPU视角下的三大并发问题
1. 缓存金字塔带来的可见性问题
现代CPU采用分级缓存架构:
- L1/L2缓存(核心独享)
- L3缓存(多核共享)
- 主内存
当线程A修改核心1缓存中的数据时,线程B在核心2缓存中看到的仍是旧值。这种内存可见性问题导致不同线程看到的数据状态不一致。
2. 流水线优化导致的原子性问题
counter++
在机器指令层面需要三个步骤:
- 从内存加载counter值到寄存器
- 对寄存器执行+1操作
- 将新值写回内存
当两个线程同时执行这三个步骤时,可能出现以下交错执行情况:
ini
Thread1: 读取counter=100
Thread2: 读取counter=100
Thread1: 计算101,写入内存
Thread2: 计算101,写入内存
最终结果只增加了1次而不是2次。
3. 指令重排序引发的有序性问题
现代CPU会进行指令重排序优化,考虑以下代码:
java
int a = 1;
boolean flag = false;
// 线程1
void write() {
a = 2; // 语句1
flag = true; // 语句2
}
// 线程2
void read() {
if(flag) { // 语句3
assert a==2; // 语句4
}
}
CPU可能将语句1和2重新排序,导致线程2看到flag=true时a仍然为1。
Java内存模型(JMM)的应对策略
内存屏障(Memory Barrier)
通过插入特定指令实现:
- LoadLoad屏障
- StoreStore屏障
- LoadStore屏障
- StoreLoad屏障
保证屏障前后的指令不会被重排序。
volatile关键字
通过内存屏障实现两大特性:
- 可见性:写操作立即刷新到主内存
- 有序性:禁止指令重排序
CAS原子指令
Compare-And-Swap的CPU指令实现无锁并发:
java
AtomicInteger atomicCounter = new AtomicInteger(0);
void safeIncrement() {
atomicCounter.incrementAndGet();
}
其底层实现依赖CPU的LOCK CMPXCHG指令。
最佳实践
- 优先使用java.util.concurrent包中的原子类
- 明确共享变量的访问边界
- 对复合操作使用synchronized
- 谨慎使用volatile保证可见性
优化后数据正确
counter++
并非原子操作。这意味着在多线程环境下,多个线程有可能同时访问并修改 counter
变量,从而产生数据竞争与不一致的结果。要解决这个问题,需保证 counter
变量的修改是线程安全的。
修改方案
有两种常用的解决办法:
- 使用
synchronized
关键字:此关键字可确保在同一时间只有一个线程能够执行同步块中的代码,从而避免数据竞争。 - 使用
AtomicInteger
类:该类提供了原子性的操作方法,能够在多线程环境下安全地对整数进行操作。
修改后的代码
使用 synchronized
关键字
ini
public class ConcurrencyExample {
private static int counter = 0;
// 定义一个静态对象作为同步锁
private static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[1000];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
// 同步块,保证同一时间只有一个线程能执行
synchronized (lock) {
counter++;
}
}
});
threads[i].start();
}
for (Thread t : threads) {
t.join();
}
System.out.println(" 预期值: 1000000, 实际值: " + counter);
}
}
使用 AtomicInteger
类
ini
import java.util.concurrent.atomic.AtomicInteger;
public class ConcurrencyExample {
// 使用 AtomicInteger 类代替普通的 int 类型
private static AtomicInteger counter = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[1000];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
// 使用原子操作递增计数器
counter.incrementAndGet();
}
});
threads[i].start();
}
for (Thread t : threads) {
t.join();
}
System.out.println("预期值: 1000000, 实际值: " + counter.get());
}
}
synchronized
关键字:synchronized (lock)
语句块保证了同一时刻只有一个线程能够执行counter++
操作,避免了多个线程同时修改counter
变量的问题。AtomicInteger
类:AtomicInteger
类内部使用了 CAS(Compare-And-Swap)机制来保证操作的原子性,incrementAndGet()
方法可以安全地对计数器进行递增操作。