1. 什么是执行线程(thread)
线程作为独立的工作线
在 Java(以及编程整体)中,执行线程是与同一程序中的其他线程并行运行的独立指令序列。想象一座工厂:每位缝纫工都有自己的工作台和任务,她独立工作,但最终共同产出整体结果。
默认情况下,Java 程序从一个线程启动——运行 main 方法的那个。但并不妨碍我们创建额外的线程,让程序的不同部分同时执行。
进程与线程:有什么区别?
- 进程——一种“重量级”的执行单元。每个进程都有自己的内存空间、自己的变量和资源。进程彼此完全隔离——即使一个“崩溃”,其他也不会受影响。
- 线程(thread)——进程内部的“轻量级”执行单元。同一进程内的所有线程共享内存和资源。这意味着它们可以轻松交换数据(但也可能轻易互相干扰)。
类比:
进程就像一套独立的公寓:有自己的墙和住户。
线程就像同一套公寓里的住户:各自忙各自的,但厨房和浴室是共享的。
在 Java 中看起来怎样?
当你启动程序时,JVM 至少会创建一个线程——主线程(main)。但你可以创建新的线程,以并行执行任务。
2. 为什么需要多线程
响应性:UI 不应该“卡死”
假设你在写一个图形程序——例如文本编辑器。用户点击“保存”,而你开始把文件长时间地写入磁盘。如果这一切都在主线程中完成,程序窗口会“冻住”:用户无法点击,光标不动,界面无响应。而如果把保存放到单独的线程——界面会保持响应,用户甚至可以反悔并关闭程序。
生活中的例子:
你打开浏览器开始下载一个大文件。如果浏览器不使用线程,你在文件下载完成之前既不能打开新标签,也不能滚动页面!
并行数据处理
假设你有一千个需要处理的文件(例如重新计算哈希或替换文本)。为什么不并行处理呢?每个线程各拿一个文件独立工作,整体工作会快上好几倍。
示例:
服务器要处理上百个客户请求。如果服务器只在一个线程里做这件事,其它客户端将会长时间排队等待。而使用线程时,每个请求都可独立处理!
利用多核处理器
现代处理器并不是一个“脑子”,而是一整个团队(多个内核),可以并行工作。如果你的程序只用一个线程,其余内核就会闲着“打扫雷”。而如果启动多个线程,所有内核都能忙起来,程序运行会更快。
有趣的事实:
就连你的手机也有多个内核,而笔记本和服务器更是有数十个!不去使用它们,就像买了辆大巴却一个人乘坐。
3. 现实中的例子
| 领域 | 多线程示例 |
|---|---|
| 文件下载 | 同时下载多个文件 |
| 用户界面(UI) | 在加载/保存数据时应用不会“卡死” |
| 服务器 | 并行处理大量网络请求 |
| 游戏 | 为物理、图形、音乐、AI 分配独立线程 |
| 即时通讯 | 接收消息、发送文件、更新界面 |
| 视频处理 | 并行处理帧数据 |
小类比:
厨师在煮汤的同时,烤箱在烤派,扫地机器人在打扫地板——这一切同时发生,晚餐就能更快准备好!
4. 多线程的潜在复杂性
竞态条件(race condition)
当多个线程同时修改同一个变量时,结果可能不可预测。比如两个线程同时递增共享计数器,最终值可能是错误的。我们会在接下来的讲座中更详细地讨论这一点。
同步
为了让线程互不干扰,就需要想办法“协商”——谁在什么时候可以修改数据。这被称为同步。为此有一些专门的关键字与结构(如 synchronized、锁等),我们稍后会讲到。
Deadlock(死锁)
有时线程会彼此“等待”,以至于永远相互等待,程序就会挂起。这就是 deadlock——它是多线程编程中最阴险的错误之一。
调试与测试
多线程程序中的错误非常难以捕捉:有时能运行,有时又不行。有些 bug 只会在服务器或用户的机器上出现,而在你的电脑上却一切完美。这让多线程代码的测试和调试成为开发者的“硬核任务”。
5. 快速概览:多线程程序是什么样
没有线程的示例:
public class Main {
public static void main(String[] args) {
// 计数到 5
for (int i = 1; i <= 5; i++) {
System.out.println(i);
}
// 输出字母
for (char c = 'A'; c <= 'E'; c++) {
System.out.println(c);
}
}
}
输出始终相同:
1
2
3
4
5
A
B
C
D
E
使用线程的示例:
public class Main {
public static void main(String[] args) {
Thread numbers = new Thread(() -> {
for (int i = 1; i <= 5; i++) {
System.out.println(i);
try {
Thread.sleep(100); // 稍微等待一下
} catch (InterruptedException e) {
// 忽略
}
}
});
Thread letters = new Thread(() -> {
for (char c = 'A'; c <= 'E'; c++) {
System.out.println(c);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// 忽略
}
}
});
numbers.start();
letters.start();
}
}
输出会被交错:
1
A
2
B
3
C
4
D
5
E
或者,如果线程“抢跑”,顺序可能不同。关键点是——两个循环是并行进行的!
6. 实用细节
可视化示意:线程如何协同工作
+-------------------+ +-------------------+
| 主线程 | | 第二个线程 |
+-------------------+ +-------------------+
| 1 | 2 | 3 | 4 | 5 | | A | B | C | D | E |
+-------------------+ +-------------------+
| |
| 同时工作 |
| 一起 |
+-------------------------+
Java 在“幕后”哪里使用了线程
- 垃圾回收(Garbage Collector)——使用独立线程清理未使用对象。
- I/O(输入/输出)线程——文件读写、网络连接。
- 服务器与 Web 应用——每个客户端请求在独立线程中处理。
- 定时器、任务调度器——按计划执行任务。
7. 新手常见错误
错误 №1:指望线程总能加速程序。
实际上,如果你的机器是单核处理器,或你组织不当,多线程可能因为“上下文切换的开销”和各种“纠缠”而让程序更慢。
错误 №2:忽视同步问题。
很多人以为:“我只是启动两个线程,能出什么问题?”但如果两个线程同时修改同一个变量,结果可能完全出乎意料。
错误 №3:到处都用线程。
不要为每个小任务都启动一个独立线程。线程是一种资源,过多的线程可能导致卡顿,甚至让程序崩溃。
错误 №4:不处理错误。
线程在处理文件或网络时可能抛出异常。如果不处理这些错误,程序可能会异常退出或“挂起”。
GO TO FULL VERSION