从CPU角度来看并发问题

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++在机器指令层面需要三个步骤:

  1. 从内存加载counter值到寄存器
  2. 对寄存器执行+1操作
  3. 将新值写回内存

当两个线程同时执行这三个步骤时,可能出现以下交错执行情况:

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关键字

通过内存屏障实现两大特性:

  1. 可见性:写操作立即刷新到主内存
  2. 有序性:禁止指令重排序

CAS原子指令

Compare-And-Swap的CPU指令实现无锁并发:

java 复制代码
AtomicInteger atomicCounter = new AtomicInteger(0);

void safeIncrement() {
    atomicCounter.incrementAndGet();
}

其底层实现依赖CPU的LOCK CMPXCHG指令。

最佳实践

  1. 优先使用java.util.concurrent包中的原子类
  2. 明确共享变量的访问边界
  3. 对复合操作使用synchronized
  4. 谨慎使用volatile保证可见性

优化后数据正确

counter++ 并非原子操作。这意味着在多线程环境下,多个线程有可能同时访问并修改 counter 变量,从而产生数据竞争与不一致的结果。要解决这个问题,需保证 counter 变量的修改是线程安全的。

修改方案

有两种常用的解决办法:

  1. 使用 synchronized 关键字:此关键字可确保在同一时间只有一个线程能够执行同步块中的代码,从而避免数据竞争。
  2. 使用 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());
    }
}
  1. synchronized 关键字:synchronized (lock) 语句块保证了同一时刻只有一个线程能够执行 counter++ 操作,避免了多个线程同时修改 counter 变量的问题。
  2. AtomicInteger 类:AtomicInteger 类内部使用了 CAS(Compare-And-Swap)机制来保证操作的原子性,incrementAndGet() 方法可以安全地对计数器进行递增操作。
相关推荐
码路飞21 分钟前
GPT-5.3 Instant 终于学会好好说话了,顺手对比了下同天发布的 Gemini 3.1 Flash-Lite
java·javascript
序安InToo24 分钟前
第6课|注释与代码风格
后端·操作系统·嵌入式
xyy12324 分钟前
C#: Newtonsoft.Json 到 System.Text.Json 迁移避坑指南
后端
洋洋技术笔记27 分钟前
Spring Boot Web MVC配置详解
spring boot·后端
JxWang0527 分钟前
VS Code 配置 Markdown 环境
后端
navms31 分钟前
搞懂线程池,先把 Worker 机制啃明白
后端
JxWang0531 分钟前
离线数仓的优化及重构
后端
Nyarlathotep011332 分钟前
gin01:初探gin的启动
后端·go
JxWang0532 分钟前
安卓手机配置通用多屏协同及自动化脚本
后端
JxWang0534 分钟前
Windows Terminal 配置 oh-my-posh
后端