Skip to content

并发的优势与挑战

天下没有免费的午餐,并发带来的性能提升是有代价的。

先看一个场景

凌晨 2 点,你的电商系统突然涌入大量用户。服务器只有 4 核 CPU。

不用并发:用户排队等待,一个请求处理完再处理下一个。用户等到超时。

用并发:多个请求同时处理,CPU 切换处理。用户响应快,服务器资源被充分利用。

这就是并发的价值——让硬件资源物尽其用

并发的优势

1. 资源利用率:CPU 不再空等

不用并发:
CPU: ████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
     ↑ 处理请求    ↑ 等待数据库响应(CPU 空转)...

用并发:
CPU: ██████████████████████████████████████████...
     ↑ 处理请求1  ↑ 处理请求2  ↑ 处理请求3

代码演示:

java
public class ResourceUtilization {

    public static void main(String[] args) throws InterruptedException {
        long start = System.nanoTime();

        // 假设有 3 个 I/O 任务,每个需要 1 秒
        // 串行:3 秒
        // 并发:约 1 秒

        ExecutorService executor = Executors.newFixedThreadPool(3);

        executor.submit(() -> task("文件1", 1000));
        executor.submit(() -> task("文件2", 1000));
        executor.submit(() -> task("文件3", 1000));

        executor.shutdown();
        executor.awaitTermination(10, TimeUnit.SECONDS);

        long duration = (System.nanoTime() - start) / 1_000_000;
        System.out.println("并发耗时: " + duration + "ms");
    }

    private static void task(String name, int ms) {
        try {
            System.out.println(name + " 开始");
            Thread.sleep(ms);
            System.out.println(name + " 完成");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

2. 响应速度:后台任务不阻塞用户

java
public class ResponsivenessDemo {

    public static void main(String[] args) {
        System.out.println("开始处理请求");

        // 耗时操作放后台线程,不阻塞主线程
        new Thread(() -> {
            // 模拟耗时任务
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("后台任务完成");
        }, "后台任务").start();

        // 主线程立即返回
        System.out.println("请求处理完成(后台任务继续运行)");
    }
}

3. 任务分解:复杂问题简单化

java
public class TaskDecomposition {

    public static void main(String[] args) {
        // 假设要处理一个大文件
        // 不用并发:从头到尾处理,耗时 T
        // 用并发:分成 4 份,4 个线程同时处理,耗时 T/4

        String bigFile = "huge_data.csv";
        int threadCount = 4;

        ExecutorService executor = Executors.newFixedThreadPool(threadCount);

        // 分成 4 段处理
        long segmentSize = getFileSize(bigFile) / threadCount;
        for (int i = 0; i < threadCount; i++) {
            long start = i * segmentSize;
            long end = (i == threadCount - 1) ? getFileSize(bigFile) : (i + 1) * segmentSize;
            executor.submit(() -> processSegment(bigFile, start, end));
        }

        executor.shutdown();
    }

    private static long getFileSize(String file) {
        return 1_000_000_000L; // 模拟
    }

    private static void processSegment(String file, long start, long end) {
        // 处理文件段
    }
}

4. 硬件利用率:充分利用多核

java
public class MultiCoreUtilization {

    public static void main(String[] args) {
        int cores = Runtime.getRuntime().availableProcessors();
        System.out.println("CPU 核心数: " + cores);

        // CPU 密集型任务
        ExecutorService executor = Executors.newFixedThreadPool(cores);

        for (int i = 0; i < cores; i++) {
            executor.submit(() -> {
                // 每个线程占用一个 CPU 核心
                long sum = 0;
                for (int j = 0; j < 1_000_000_000; j++) {
                    sum += j;
                }
            });
        }

        executor.shutdown();
    }
}

并发的挑战

1. 线程安全:共享数据的一致性

这是并发最核心的问题。

java
public class RaceConditionDemo {

    private int counter = 0;  // 共享变量

    public void increment() {
        counter++;  // ❌ 不是原子操作!
    }

    public static void main(String[] args) throws InterruptedException {
        RaceConditionDemo demo = new RaceConditionDemo();

        // 1000 个线程,每个累加 1000 次
        // 理论上结果应该是 1000000
        for (int i = 0; i < 1000; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    demo.increment();
                }
            }).start();
        }

        Thread.sleep(5000);  // 等待所有线程完成
        System.out.println("预期: 1000000, 实际: " + demo.counter);
        // 结果可能小于 1000000,因为 counter++ 不是原子的
    }
}

为什么 counter++ 不是原子的?

counter++ 实际分三步:
1. 从内存读取 counter 到 CPU 寄存器
2. 在 CPU 中执行 +1 操作
3. 把结果写回内存

线程 A:读取 counter=0  → 计算 1  → 还没写回
线程 B:读取 counter=0  → 计算 1  → 写回 counter=1
线程 A:写回 counter=1  → 结果:counter=1(丢失了线程 B 的更新)

2. 死锁:互相等待对方释放锁

java
public class DeadlockDemo {

    private static final Object lockA = new Object();
    private static final Object lockB = new Object();

    public static void main(String[] args) {
        // 线程 1:先锁 A,再锁 B
        new Thread(() -> {
            synchronized (lockA) {
                System.out.println("线程1: 持有锁A");
                try { Thread.sleep(100); } catch (InterruptedException e) {}
                synchronized (lockB) {
                    System.out.println("线程1: 持有锁B");
                }
            }
        }, "Thread-1").start();

        // 线程 2:先锁 B,再锁 A
        new Thread(() -> {
            synchronized (lockB) {
                System.out.println("线程2: 持有锁B");
                try { Thread.sleep(100); } catch (InterruptedException e) {}
                synchronized (lockA) {
                    System.out.println("线程2: 持有锁A");
                }
            }
        }, "Thread-2").start();

        // 死锁!线程1等线程2释放B,线程2等线程1释放A
    }
}

3. 可见性问题:一个线程的修改,另一个线程看不到

java
public class VisibilityDemo {

    private static boolean ready = false;
    private static int number = 0;

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            while (!ready) {
                // 理论上,ready 变成 true 时,循环应该退出
                // 但由于可见性问题,可能永远看不到 ready=true
            }
            System.out.println(number);  // 可能输出 0
        }).start();

        Thread.sleep(100);  // 确保上面的线程先启动

        new Thread(() -> {
            number = 42;
            ready = true;  // 修改后,其他线程可能看不到
        }).start();
    }
}

4. 活锁:线程一直在运行,但工作没有进展

java
public class LivelockDemo {

    // 两个线程都在运行,但都在礼让对方
    public static void main(String[] args) {
        AtomicBoolean aliceTurn = new AtomicBoolean(true);
        AtomicBoolean bobTurn = new AtomicBoolean(false);

        // Alice
        new Thread(() -> {
            while (true) {
                if (aliceTurn.compareAndSet(true, false)) {
                    System.out.println("Alice 完成任务");
                    bobTurn.set(true);
                }
                // 模拟处理时间很短,可能导致不断切换
                Thread.yield();
            }
        }).start();

        // Bob
        new Thread(() -> {
            while (true) {
                if (bobTurn.compareAndSet(true, false)) {
                    System.out.println("Bob 完成任务");
                    aliceTurn.set(true);
                }
                Thread.yield();
            }
        }).start();
    }
}

优势 vs 挑战对比

场景不用并发用并发
CPU 利用率低(单核)高(多核)
I/O 等待阻塞切换执行其他任务
响应时间短(后台任务不阻塞)
吞吐量
开发复杂度简单复杂
调试难度
数据安全无问题需要同步

何时用并发?

适合并发的场景

  • I/O 密集型(等待时间长)
  • 需要并行计算(多核 CPU)
  • 需要高吞吐量
  • 需要快速响应(后台任务不阻塞)

不适合并发的场景

  • 任务太短(创建线程开销 > 任务本身)
  • 任务之间有强依赖(无法并行)
  • 单核 CPU 且无 I/O(并发反而增加开销)

总结

并发是双刃剑:

  • 优势:充分利用硬件、提高响应速度、提升吞吐量
  • 挑战:线程安全、死锁、可见性、活锁

关键原则

  1. 不要过度并发
  2. 避免不必要的共享
  3. 使用不可变对象
  4. 选择合适的同步机制
  5. 优先使用高级抽象(线程池、并发工具类)

下一节我们深入探讨线程安全问题。

基于 VitePress 构建