Java初级并发知识【1】,看看你还记得多少?
你好,我是一航同学,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:进程和线程,哪个更"轻量"?为什么?
线程更轻量,原因:
- 创建/销毁线程不需要分配独立内存空间
- 线程切换只需保存少量寄存器状态,而进程切换要切换页表、刷新 TLB 等
- 线程间通信直接读写共享变量,进程间通信需要内核介入
Q3:多线程一定比单线程快吗?
不一定!需要分场景:
- 适合多线程: IO 密集型(如查数据库、调接口),线程等待时可切换执行其他任务
- 可能更慢: CPU 密集型 + 线程数 > 核数,频繁上下文切换反而消耗性能
- 额外风险: 线程安全、死锁、资源竞争等问题可能引入新 bug
自测练习
- 【判断】
new Thread().run()能启动新线程吗?为什么? - 【选择】 以下哪项是线程共享的?(多选)A. 堆内存 B. 方法区 C. 程序计数器 D. 线程栈
- 【简答】 为什么浏览器每个标签页通常是一个独立进程,而不是线程?
- 【代码】 写一段代码,创建 2 个线程,分别打印"线程A执行"和"线程B执行"
答案
- 不能。
run()只是普通方法调用,仍在当前线程执行;必须用start()让 JVM 调度新线程。 - A、B。 堆和方法区属于进程(JVM)级资源,所有线程共享;程序计数器和栈是线程私有的。
- 隔离性和健壮性: 一个标签页崩溃(如 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 的核心思想)。
自测练习
- 【判断】 "异步"一定比"同步"快吗?为什么?
- 【选择】 以下哪种场景最适合用异步编程?A. 计算斐波那契数列 B. 并行调用 3 个第三方接口 C. 单线程更新本地变量
- 【简答】 用一句话解释"并发"和"并行"的区别,让非技术人员也能听懂
- 【代码】 把下面的同步代码改成异步(用 CompletableFuture):
// 同步:串行调用
String result = serviceA.call() + serviceB.call();
答案
- 不一定! 异步的优势是不阻塞线程,提升吞吐量,但单次请求的延迟可能不变甚至略高(因为有回调开销)。适合 IO 密集型,不适合纯 CPU 计算。
- B。 异步适合有等待场景(如网络调用),能并行发起 + 非阻塞等待;A 是 CPU 计算,C 无等待,异步反而增加复杂度。
- 并发就像一个收银员同时服务 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() 仅演示,生产慎用阻塞
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)