从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() 方法可以安全地对计数器进行递增操作。
相关推荐
Jay Kay1 小时前
TensorFlow内核剖析:分布式TensorFlow架构解析与实战指南
分布式·架构·tensorflow
亿牛云爬虫专家3 小时前
Kubernetes下的分布式采集系统设计与实战:趋势监测失效引发的架构进化
分布式·python·架构·kubernetes·爬虫代理·监测·采集
kangkang-3 小时前
PC端基于SpringBoot架构控制无人机(三):系统架构设计
java·架构·无人机
界面开发小八哥5 小时前
「Java EE开发指南」如何用MyEclipse创建一个WEB项目?(三)
java·ide·java-ee·myeclipse
ai小鬼头5 小时前
Ollama+OpenWeb最新版0.42+0.3.35一键安装教程,轻松搞定AI模型部署
后端·架构·github
idolyXyz5 小时前
[java: Cleaner]-一文述之
java
一碗谦谦粉6 小时前
Maven 依赖调解的两大原则
java·maven
萧曵 丶6 小时前
Rust 所有权系统:深入浅出指南
开发语言·后端·rust
netyeaxi6 小时前
Java:使用spring-boot + mybatis如何打印SQL日志?
java·spring·mybatis
收破烂的小熊猫~6 小时前
《Java修仙传:从凡胎到码帝》第四章:设计模式破万法
java·开发语言·设计模式