你好,我是一航同学,AI发展很快,快到我们已经忽略了很多基础知识的构建。俗话说,基础不牢地动山摇。在快速迭代的AI时代,请沉下心,耐心看完,查缺补漏。

Java初级并发知识【1】

进程 VS 线程

核心概念

进程(Process)是资源分配的最小单位,线程(Thread)是 CPU 调度的最小单位。
一个进程可以包含多个线程,它们共享进程的资源,但各自有独立的执行栈。


生活类比:奶茶店模型

概念 类比 说明
进程 奶茶店 有独立的店面(内存空间)、设备(文件句柄)、原料(系统资源)
线程 店员 共享店里的资源(奶粉、杯子),但各自独立接单、制作(执行任务)
任务 订单 线程从队列取任务执行,模拟并发处理

关键推论:

  • 店员(线程)之间可以快速沟通(共享内存)
  • 一个店员晕倒(线程崩溃),不会直接导致整家店倒闭(进程可能还能救)
  • 但如果奶茶店被查封(进程结束),所有店员(线程)立刻全部停止工作

Java 代码视角

1. 启动 Java 程序 = 启动一个 JVM 进程
java MyApplication

这条命令会让操作系统创建一个 JVM 进程,分配独立内存空间。

2. 默认至少有一个线程:main 线程
public class Hello {
    public static void main(String[] args) {
        // 这行代码运行在「main线程」中
        System.out.println("当前线程: " + Thread.currentThread().getName());
        // 输出: 当前线程: main
    }
}
3. 手动创建新线程(三种方式)
// 方式1:继承Thread类
class MyThread extends Thread {
    public void run() {
        System.out.println("新线程执行: " + getName());
    }
}

// 方式2:实现Runnable接口(更推荐,避免单继承限制)
class MyTask implements Runnable {
    public void run() {
        System.out.println("任务在线程: " + Thread.currentThread().getName() + " 中执行");
    }
}

// 使用
public class Demo {
    public static void main(String[] args) {
        new MyThread().start();  // 启动新线程

        new Thread(new MyTask()).start();  // Runnable方式

        // Lambda简洁写法(Java8+)
        new Thread(() -> System.out.println("Lambda线程运行!")).start();
    }
}

注意: 调用 start() 而不是 run()

  • start() → 启动新线程,JVM 调度执行 run() 方法
  • run() → 只是普通方法调用,仍在当前线程执行,没有并发效果

对比表

对比维度 进程(Process) 线程(Thread) 理解要点
资源归属 独立内存空间、文件句柄等 共享所属进程的资源 线程"轻",因为不用重复分配资源
切换开销 大(要切换页表、缓存等) 小(只需切换栈和寄存器) 多线程并发效率更高
通信方式 复杂(IPC:管道/队列/共享内存等) 简单(直接读写共享变量) 但共享变量需注意线程安全!
健壮性 一个进程崩溃不影响其他进程 一个线程崩溃可能导致整个进程结束 Java中未捕获的线程异常会打印栈但进程可能继续
创建方式 操作系统API(如fork) Java中 new Thread() / 线程池 日常开发 99% 操作的是线程

高频面试题

Q1:Java 程序启动后,默认有几个线程?

public class Test {
    public static void main(String[] args) {
        System.out.println(Thread.activeCount());
    }
}

不止一个!除了 main 线程,JVM 还会启动:

  • Reference Handler(处理引用对象)
  • Finalizer(执行 finalize 方法)
  • Signal Dispatcher(处理系统信号)
  • Attach Listener(可能存在,用于动态 attach)

所以通常输出是 3~5 个,具体取决于 JVM 实现和参数。


Q2:进程和线程,哪个更"轻量"?为什么?

线程更轻量,原因:

  1. 创建/销毁线程不需要分配独立内存空间
  2. 线程切换只需保存少量寄存器状态,而进程切换要切换页表、刷新 TLB
  3. 线程间通信直接读写共享变量,进程间通信需要内核介入

Q3:多线程一定比单线程快吗?

不一定!需要分场景:

  1. 适合多线程: IO 密集型(如查数据库、调接口),线程等待时可切换执行其他任务
  2. 可能更慢: CPU 密集型 + 线程数 > 核数,频繁上下文切换反而消耗性能
  3. 额外风险: 线程安全、死锁、资源竞争等问题可能引入新 bug

自测练习

  1. 【判断】 new Thread().run() 能启动新线程吗?为什么?
  2. 【选择】 以下哪项是线程共享的?(多选)A. 堆内存 B. 方法区 C. 程序计数器 D. 线程栈
  3. 【简答】 为什么浏览器每个标签页通常是一个独立进程,而不是线程?
  4. 【代码】 写一段代码,创建 2 个线程,分别打印"线程A执行"和"线程B执行"

答案

  1. 不能。 run() 只是普通方法调用,仍在当前线程执行;必须用 start() 让 JVM 调度新线程。
  2. A、B。 堆和方法区属于进程(JVM)级资源,所有线程共享;程序计数器和栈是线程私有的。
  3. 隔离性和健壮性: 一个标签页崩溃(如 JS 死循环)不会影响其他标签页;进程间内存隔离更安全。
new Thread(() -> System.out.println("线程A执行")).start();
new Thread(() -> System.out.println("线程B执行")).start();

并发 vs 并行 + 同步 vs 异步

核心概念

概念对 关注点 核心区别
并发 vs 并行 任务执行的"时间关系" 并发是"交替做",并行是"同时做"
同步 vs 异步 调用方"等不等结果" 同步是"等着办完",异步是"办完叫我"

这两组概念是正交的! 可以组合出 4 种场景:同步并发、异步并发、同步并行、异步并行。日常开发最常见的是异步并发


生活类比:奶茶店 + 点单

1. 并发 vs 并行:一个厨师 vs 三个厨师
场景 类比 对应概念 说明
单厨师 + 3 个订单 厨师轮流炒:炒 2 分钟菜 A → 翻一下菜 B → 加调料菜 C → 再回菜 A… 并发(Concurrency) 宏观上 3 个菜"同时在做",微观上是时间片轮转,单核 CPU 就是这样工作的
三厨师 + 3 个订单 每个厨师专注炒一个菜,真正同时开火 并行(Parallelism) 需要多核 CPU,多个线程真正同时执行

关键结论:

  • 并发强调结构设计(能处理多个任务),并行强调物理执行(真的同时跑)
  • 单核 CPU 只能并发,不能并行;多核 CPU 才能并行
  • Java 程序默认利用并发,想利用并行需要:① 多核机器 ② 合理设计多线程
2. 同步 vs 异步:点奶茶的两种方式
方式 类比 对应概念 代码特征
站在柜台等 点单 → 站着等 → 拿到奶茶 → 离开 同步(Synchronous) 调用方法后阻塞等待返回结果,才执行下一行
拿号玩手机 点单 → 拿号码牌 → 去旁边玩手机 → 叫号取餐 异步(Asynchronous) 调用方法后立即返回,结果通过回调/Future/CompletableFuture 通知

关键结论:

  • 同步/异步关注的是调用方的体验:要不要"傻等"
  • 异步 ≠ 并发! 异步可以用单线程实现(如 JavaScript 事件循环)
  • Java 中异步编程常用:Future、CompletableFuture、回调接口

Java 代码视角

1. 并发 vs 并行:代码一样,硬件决定
// 这段代码在单核/多核机器上都能运行
for (int i = 0; i < 3; i++) {
    final int taskId = i;
    new Thread(() -> {
        System.out.println("任务" + taskId + "执行中...");
        try { Thread.sleep(1000); } catch (InterruptedException e) {}
    }).start();
}
运行环境 实际执行效果 属于
单核 CPU 3 个线程交替执行,宏观上"同时" 并发
4 核 CPU 3 个线程可能真正同时在 3 个核上跑 并行

写代码时通常说"用多线程实现并发",是否能并行交给操作系统和硬件决定。

2. 同步 vs 异步:代码写法完全不同

同步写法(阻塞等待)

// 模拟:查询用户 + 查询订单(同步串行)
public UserOrder syncGetUserOrder(Long userId) {
    // 必须等 getUser 执行完,才执行 getOrder
    User user = getUser(userId);      // 耗时 100ms,当前线程阻塞等待
    Order order = getOrder(userId);   // 耗时 100ms,当前线程阻塞等待
    return new UserOrder(user, order);
    // 总耗时 ≈ 200ms
}

异步写法(非阻塞 + 回调)

// 模拟:查询用户 + 查询订单(异步并行)
public CompletableFuture<UserOrder> asyncGetUserOrder(Long userId) {
    // 两个任务异步提交,可能并行执行
    CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(() -> getUser(userId));
    CompletableFuture<Order> orderFuture = CompletableFuture.supplyAsync(() -> getOrder(userId));

    // 等两个结果都返回后,组合结果
    return userFuture.thenCombine(orderFuture, (user, order) -> {
        return new UserOrder(user, order);
    });
    // 总耗时 ≈ max(100ms, 100ms) = 100ms
}

// 调用方可以:
// ① 继续做其他事(非阻塞)
// ② 用 .get() 阻塞等待(不推荐)
// ③ 链式编排更多异步任务

对比表

概念 关注点 核心问题 类比 Java 典型实现
并发 任务调度 “能不能处理多个任务?” 单厨师炒多道菜 Thread、线程池
并行 物理执行 “能不能真正同时跑?” 多厨师同时炒菜 多核 CPU + 多线程
同步 调用体验 “我要不要等结果?” 站着等奶茶 普通方法调用
异步 调用体验 “结果怎么通知我?” 拿号玩手机 CompletableFuture、回调

常见组合场景

组合 场景举例 常用程度
同步 + 并发 Servlet 处理多个请求(每个请求同步处理,但多请求并发) ⭐⭐⭐⭐⭐
异步 + 并发 用 CompletableFuture 并行调用 3 个接口,聚合结果 ⭐⭐⭐⭐⭐
同步 + 并行 多线程计算矩阵乘法(需多核 + 任务可拆分) ⭐⭐⭐
异步 + 并行 分布式系统 + 异步消息(高级场景) ⭐⭐

高频面试题

Q1:单核 CPU 能实现并发吗?能实现并行吗?

  • 能实现并发: 通过时间片轮转,线程交替执行,宏观上"同时"
  • 不能实现并行: 并行需要多个执行单元(多核/多 CPU),单核同一时刻只能执行一条指令

Q2:异步和并发是什么关系?

它们是不同维度的概念:

  • 并发: 多个任务"宏观同时"执行(调度层面)
  • 异步: 调用方不等待结果(编程模型层面)

可以组合:比如用单线程 + 事件循环实现异步并发(如 Node.js);也可以用多线程实现同步并发(如传统 Java Web)。


Q3:CompletableFuture 怎么实现异步编排?举个简单例子。

// 场景:异步查询用户 + 积分 + 等级,组合结果
CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(() -> getUser(id));
CompletableFuture<Integer> pointFuture = CompletableFuture.supplyAsync(() -> getPoint(id));
CompletableFuture<String> levelFuture = CompletableFuture.supplyAsync(() -> getLevel(id));

// 等三个结果都返回后,组合成 DTO
CompletableFuture<UserDetail> result = userFuture
    .thenCombine(pointFuture, (user, point) -> new UserDetail(user, point))
    .thenCombine(levelFuture, (detail, level) -> {
        detail.setLevel(level);
        return detail;
    });

// 调用方可注册回调,或按需阻塞等待
result.thenAccept(detail -> System.out.println("查询完成: " + detail));

Q4:为什么高并发系统喜欢用"异步非阻塞"?

因为能提升资源利用率:

  • 同步阻塞: 线程等待 IO 时(如查库、调接口)不能干别的,浪费线程资源
  • 异步非阻塞: 线程提交请求后立即返回,可去处理其他任务,等结果通过回调通知

核心思想: 用更少的线程支撑更高的并发(如 Netty、WebFlux 的核心思想)。


自测练习

  1. 【判断】 "异步"一定比"同步"快吗?为什么?
  2. 【选择】 以下哪种场景最适合用异步编程?A. 计算斐波那契数列 B. 并行调用 3 个第三方接口 C. 单线程更新本地变量
  3. 【简答】 用一句话解释"并发"和"并行"的区别,让非技术人员也能听懂
  4. 【代码】 把下面的同步代码改成异步(用 CompletableFuture):
// 同步:串行调用
String result = serviceA.call() + serviceB.call();

答案

  1. 不一定! 异步的优势是不阻塞线程,提升吞吐量,但单次请求的延迟可能不变甚至略高(因为有回调开销)。适合 IO 密集型,不适合纯 CPU 计算。
  2. B。 异步适合有等待场景(如网络调用),能并行发起 + 非阻塞等待;A 是 CPU 计算,C 无等待,异步反而增加复杂度。
  3. 并发就像一个收银员同时服务 3 个顾客(交替扫码),并行就像 3 个收银员同时服务 3 个顾客(真正同时)。
CompletableFuture<String> aFuture = CompletableFuture.supplyAsync(() -> serviceA.call());
CompletableFuture<String> bFuture = CompletableFuture.supplyAsync(() -> serviceB.call());
String result = aFuture.thenCombine(bFuture, (a, b) -> a + b).join(); // join() 仅演示,生产慎用阻塞
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐