为什么需要并发编程

📋 概述

一句话:并发编程是为了解决"单线程处理不过来"的问题------让程序能同时做多件事,提升性能和响应速度。

从一个真实场景说起

想象一下:你开了一家网店,平时一天卖 100 单,用一个客服处理订单,绰绰有余。

突然双十一来了,同时涌入 1000 个订单。如果你还坚持"一个客服处理完一个再处理下一个",会发生什么?

  • 第一个用户等了 10 分钟才拿到确认
  • 第十个用户等了 100 分钟,直接退款走人
  • 第一百个用户等到页面超时,再也不来了

问题的本质:单线程(单客服)的处理能力是有限的,当请求量超过处理速度时,系统就会崩溃。

解决方案 :多雇几个客服(多线程),同时处理多个订单。这就是并发编程的动机

💡 生活类比:厨房做饭

单线程:一个人做饭

想象你一个人在厨房做饭,流程是这样的:

总耗时 :5 + 3 + 5 + 1 = 14 分钟。在这 14 分钟里,你一直在忙,但效率很低------洗菜时灶台是空的,切菜时水龙头是空的。

多线程:厨房多人协作

现在雇了三个人:你负责切菜,小王负责洗菜,小李负责炒菜。三个人分工合作:

总耗时 :可能 5 分钟 就能上好几道菜。因为三个人同时在做不同的事。

但是,多人协作也会出问题

假设你和小王都想用同一个锅

  • 你正在炒菜,小王也想炒菜
  • 两个人抢一个锅,结果菜洒了,锅也坏了

这就是并发编程中的资源争抢 问题。解决方法是什么?加锁 ------"这个锅我在用,你等一下"。这就是后续章节要讲的 synchronizedLock

🔍 单线程的 3 大瓶颈

在现代计算机中,单线程程序有三个致命问题:

瓶颈 问题描述 生活类比 影响
CPU 利用率低 等待 I/O(网络/磁盘/数据库)时 CPU 空转 厨师等外卖食材,站在厨房发呆 资源浪费,处理速度慢
响应性差 一个任务卡住,整个程序卡住 厨师等食材,所有客人都饿着 用户体验差,系统不可用
吞吐量低 无法利用多核 CPU 厨房有 4 个灶台,只用 1 个 性能天花板低

代码演示:单线程的低效

arduino 复制代码
public class SingleThreadDemo {
    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        
        // 模拟 3 个耗时任务(每个 1 秒)
        task("任务1", 1000);
        task("任务2", 1000);
        task("任务3", 1000);
        
        long end = System.currentTimeMillis();
        System.out.println("总耗时: " + (end - start) + "ms");
    }
    
    static void task(String name, long sleepTime) {
        try {
            System.out.println("开始执行: " + name);
            Thread.sleep(sleepTime);  // 模拟 I/O 等待
            System.out.println("完成: " + name);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

预期输出

makefile 复制代码
开始执行: 任务1
完成: 任务1
开始执行: 任务2
完成: 任务2
开始执行: 任务3
完成: 任务3
总耗时: 3000ms

问题 :3 个任务串行执行,总耗时 = 任务1 + 任务2 + 任务3 = 3 秒。在等待任务1完成时,CPU 是空闲的,但没人用它。

🔍 并发解决了什么问题

并发编程通过"同时处理多个任务"来解决单线程的瓶颈:

解决方案 原理 适用场景 效果
多核并行计算 多个 CPU 核心同时执行不同任务 CPU 密集型(计算、加密) 性能提升 N 倍(N=核心数)
异步处理 主线程不等待,任务完成后回调 I/O 密集型(网络、数据库) 响应时间大幅降低
资源利用 等待时让出 CPU 给其他任务 混合场景 系统整体吞吐量提升

代码演示:多线程并行处理

scss 复制代码
public class MultiThreadDemo {
    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        
        // 创建 3 个线程同时执行
        Thread t1 = new Thread(() -> task("任务1", 1000));
        Thread t2 = new Thread(() -> task("任务2", 1000));
        Thread t3 = new Thread(() -> task("任务3", 1000));
        
        t1.start();
        t2.start();
        t3.start();
        
        try {
            t1.join();  // 等待 t1 完成
            t2.join();  // 等待 t2 完成
            t3.join();  // 等待 t3 完成
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        long end = System.currentTimeMillis();
        System.out.println("总耗时: " + (end - start) + "ms");
    }
    
    static void task(String name, long sleepTime) {
        try {
            System.out.println("开始执行: " + name);
            Thread.sleep(sleepTime);
            System.out.println("完成: " + name);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

预期输出

makefile 复制代码
开始执行: 任务1
开始执行: 任务2
开始执行: 任务3
完成: 任务1
完成: 任务2
完成: 任务3
总耗时: 1000ms

对比 :多线程总耗时 1 秒 ,是单线程的 1/3 。因为 3 个任务同时执行。

🔍 并发的演进

Java 并发编程经历了 20 多年的演进:

版本 新增特性 解决的问题
JDK 1.0 Threadsynchronized 基础多线程支持
JDK 5.0 java.util.concurrent 高效并发工具、线程池、并发集合
JDK 8 CompletableFuture、Lambda 异步编程简化
JDK 21 虚拟线程、结构化并发 轻量级线程、更好的并发控制

💻 可跑代码:3 个完整示例

示例 1:单线程处理(慢)

csharp 复制代码
public class SlowServer {
    public static void main(String[] args) {
        System.out.println("=== 单线程处理 3 个请求 ===");
        long start = System.currentTimeMillis();
        
        // 模拟处理 3 个请求
        handleRequest("用户A查询订单");
        handleRequest("用户B提交支付");
        handleRequest("用户C下载发票");
        
        long end = System.currentTimeMillis();
        System.out.println("总耗时: " + (end - start) + "ms");
        System.out.println("用户平均等待: " + (end - start) / 3 + "ms");
    }
    
    static void handleRequest(String request) {
        try {
            System.out.println("处理: " + request);
            Thread.sleep(1000);  // 模拟网络 I/O
            System.out.println("完成: " + request);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

预期输出

makefile 复制代码
=== 单线程处理 3 个请求 ===
处理: 用户A查询订单
完成: 用户A查询订单
处理: 用户B提交支付
完成: 用户B提交支付
处理: 用户C下载发票
完成: 用户C下载发票
总耗时: 3000ms
用户平均等待: 1000ms

示例 2:多线程并行处理(快)

csharp 复制代码
public class FastServer {
    public static void main(String[] args) {
        System.out.println("=== 多线程处理 3 个请求 ===");
        long start = System.currentTimeMillis();
        
        // 创建 3 个线程并行处理
        Thread t1 = new Thread(() -> handleRequest("用户A查询订单"));
        Thread t2 = new Thread(() -> handleRequest("用户B提交支付"));
        Thread t3 = new Thread(() -> handleRequest("用户C下载发票"));
        
        t1.start();
        t2.start();
        t3.start();
        
        // 等待所有线程完成
        try {
            t1.join();
            t2.join();
            t3.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        long end = System.currentTimeMillis();
        System.out.println("总耗时: " + (end - start) + "ms");
        System.out.println("用户平均等待: " + (end - start) / 3 + "ms");
    }
    
    static void handleRequest(String request) {
        try {
            System.out.println("处理: " + request);
            Thread.sleep(1000);
            System.out.println("完成: " + request);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

预期输出

makefile 复制代码
=== 多线程处理 3 个请求 ===
处理: 用户A查询订单
处理: 用户B提交支付
处理: 用户C下载发票
完成: 用户A查询订单
完成: 用户B提交支付
完成: 用户C下载发票
总耗时: 1000ms
用户平均等待: 333ms

示例 3:线程不安全的反例(引出后续模块)

csharp 复制代码
public class UnsafeCounter {
    private static int count = 0;
    
    public static void main(String[] args) throws InterruptedException {
        System.out.println("=== 线程不安全示例 ===");
        
        // 两个线程各加 10000 次
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                count++;  // 这行不是原子操作!
            }
        });
        
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                count++;  // 这行不是原子操作!
            }
        });
        
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        
        System.out.println("期望值: 20000");
        System.out.println("实际值: " + count);
        System.out.println("差值: " + (20000 - count));
        
        // 为什么不是 20000?
        // 因为 count++ 不是原子操作,多线程下会丢失更新
        // 后续章节会详细讲解 CAS 和 synchronized
    }
}

预期输出(每次运行可能不同):

makefile 复制代码
=== 线程不安全示例===
期望值: 20000
实际值: 15234
差值: 4766

问题分析count++ 看起来是一行代码,但实际上分三步:

  1. 读取 count 的值
  2. 加 1
  3. 写回 count

当两个线程同时执行时,可能读到相同的值,导致加 1 后覆盖对方的结果。这就是竞态条件

⚠️ 并发的代价(预告)

多人协作虽然快,但也会出问题:

问题 描述 生活类比 后续章节
竞态条件 多个线程同时修改共享数据,结果不可预期 两个人同时改同一个文档 02_原理与实现/04_锁体系总览
死锁 两个线程互相等待对方释放锁,卡死 两个人互相让路,结果都卡在门口 03_问题与应对/03_并发问题排查实战
可见性问题 一个线程修改了变量,另一个线程看不到 一个人在黑板上写字,另一个人没看到 02_原理与实现/03_并发内存模型
线程安全 多线程下数据结构可能损坏 多人同时写账本,账本可能乱套 04_应用/07_并发集合详解

怎么解决? 后面的篇目会一一拆解:

  • 锁机制(synchronized、ReentrantLock)
  • 原子类(AtomicInteger、CAS)
  • volatile 关键字
  • 并发集合(ConcurrentHashMap)
  • 线程池和同步器
ini 复制代码
// 竞态条件示例:两个人同时改同一个文档
public class RaceConditionExample {
    private static int balance = 100;
    
    public static void main(String[] args) throws InterruptedException {
        // 模拟两个线程同时扣款
        Thread t1 = new Thread(() -> {
            int temp = balance;
            try { Thread.sleep(10); } catch (Exception e) {}
            balance = temp - 50;
            System.out.println("用户A扣款后余额: " + balance);
        });
        
        Thread t2 = new Thread(() -> {
            int temp = balance;
            try { Thread.sleep(10); } catch (Exception e) {}
            balance = temp - 50;
            System.out.println("用户B扣款后余额: " + balance);
        });
        
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        
        System.out.println("最终余额: " + balance);
        System.out.println("期望余额: 0, 实际: " + balance + " (应该为0)");
    }
}

🎯 学习路径

作为专栏入口,这里告诉你接下来的模块怎么走:

学习阶段 核心内容 你会收获
入门 线程基础、内存模型、volatile、synchronized 理解并发的基本概念,能写简单的线程安全代码
进阶 锁机制、AQS、线程池、CompletableFuture 看懂 JUC 源码,会设计并发方案
实战 死锁排查、设计模式、虚拟线程 能排查线上问题,能设计并发架构

💡 面试要点

  1. 为什么用多线程?

    • 提升性能:多核 CPU 并行计算
    • 提升响应:I/O 等待时 CPU 做其他事
    • 提升吞吐:同时处理更多请求
  2. 并发 vs 并行区别?

    • 并发:多个任务交替执行(单核也能做到)
    • 并行:多个任务同时执行(需要多核)
  3. 并发的代价?

    • 竞态条件:数据不一致
    • 死锁:线程互相等待
    • 可见性:修改不可见
    • 复杂度:代码更难写、更难测
  4. 单线程 vs 多线程性能对比?

    • CPU 密集型:多线程 ≈ 单线程(甚至更慢,因为线程切换开销)
    • I/O 密集型:多线程 >> 单线程(等待时做其他事)
  5. 如何避免线程安全问题?

    • 加锁:synchronized、ReentrantLock
    • 原子类:AtomicInteger、CAS
    • volatile:保证可见性
    • 并发集合:ConcurrentHashMap

📝 总结

要点 记住这一句
动机 单线程处理不过来,需要多线程并行
类比 一个人做饭 vs 厨房多人协作
瓶颈 CPU 空转、响应差、吞吐低
解决 多核并行、异步处理、资源复用
代价 竞态、死锁、可见性问题
演进 Thread → JUC → 虚拟线程
下一步 学线程基础,理解线程是什么

一句话总结:并发编程是为了解决"慢"的问题,但引入了"乱"的问题------后面的章节会教你如何"又快又稳"。

相关推荐
Cosolar17 小时前
AI Agent 架构原理详解:从一次提问到任务完成的完整闭环
人工智能·后端·架构
Conan在掘金17 小时前
鸿蒙 ArkUI 深水区:Stage 模型多维状态,从「组件内」跳到「应用级」的分水岭
后端
长栎18 小时前
用AI优化Redis缓存失效,我把热key问题想简单了
后端
面试鸭18 小时前
我说我们做 Agent 从来不怕死循环,面试官调出兜底日志:“第 47 次强制终止,是你半夜爬起来按停的?”
后端·面试·求职
小小猪的春天18 小时前
团队级 Code Review Skill 实践:4个人、4个AI、1套规则,CR从找bug变成做决策
后端·架构
长栎18 小时前
AI帮我写了个Spring Boot校验,线上漏掉了这组边界条件
后端
外滩运维专家18 小时前
Let's Encrypt 的 ACME 协议到底干了什么?用抓包带你看一遍
后端
程序员多吃鸭eatmoreduck18 小时前
# 开源初体验,39 天 800 Star:我把项目发到了哪里,流量到底从哪来
后端