Java多线程:从入门到高效并发
Java多线程入门
在 Java 的世界里,多线程(Multi-threading)是每个进阶开发者的必经之路。如果你觉得这个词听起来很高级,别担心,今天我们用最直白的方式拆解它。
1. 概念热身:什么是进程和线程?
别去背书本上的定义,看这个类比:
你的电脑就像一个大型商场,每个正在运行的应用程序(比如微信、浏览器、你的 Java 程序)就是一个进程。进程就像商场里一家独立的店铺,有自己的场地、资源和员工。
而线程,就是店铺里真正干活的员工。
一家店(进程)至少要有一个员工(线程)才能营业,这个员工叫主线程。
一家店可以同时雇佣多个员工(多线程)一起干活,共享店里的冰箱、灶台等资源(内存)。
员工之间可以并行工作:你切菜,我炒菜,他打包,效率瞬间拉满。
2. 为什么要用多线程?
核心目的:压榨 CPU 性能,提高效率。
想象一个快餐店:
店里只有一个厨师(单线程)。客人来了要点餐,厨师先去记菜单,然后去炒菜,炒完菜出来打包,再叫客人取餐。这个过程里,他炒菜的时候就没法接待下一位客人,所有人都得排队等着。
现在我们雇了3个厨师(多线程):
厨师A专门负责点餐接单
厨师B专门负责炒菜
厨师C专门负责打包出餐
三个人同时干活,客人流动速度直接翻三倍,CPU这个“灶台”被压榨得明明白白。
多线程做的就是这样的事:把一个大任务拆成多个小任务,让 CPU 的多个核心(多个灶眼)同时运转,不让硬件闲着。
3. 核心术语大白话
新手最容易晕这几个词,我们挨个“翻译”:
| 术语 | 大白话 | 快餐店比喻 |
|---|---|---|
| 线程 (Thread) | 正在干活的员工,一个执行流程的独立单元 | 厨师本人 |
| 任务 (Runnable/Callable) | 要做什么事,具体的操作步骤 | 菜谱:先放油,再放鸡块,炸5分钟 |
| 执行者 (Executor) | 负责分配任务给线程的“排班经理” | 厨师长站在出菜口喊:“3号单给小王,5号单给小李” |
| 锁 (Lock / synchronized) | 抢控制权:同一时刻只允许一个人碰的数据或资源 | 只有一个盐罐,谁拿到谁才能放盐,防止两个人同时往里倒盐洒一地 |
| 死锁 (Deadlock) | 互相等对方手里的东西,结果谁也不能动 | 厨师A拿着盐等油,厨师B拿着油等盐,俩人互不相让,厨房瘫痪 |
| 线程池 (Thread Pool) | 固定数量的员工,有活了就干,没活就待命 | 一个3人厨师小组,用完不辞退,下个任务直接给他们 |
| 上下文切换 | CPU 在不同线程之间快速“换人”干活 | 厨师切菜切到一半,被喊去收银,回来再接着切——切换本身也消耗时间,所以线程不是越多越好 |
掌握了这些词,你基本就拿到了多线程世界的“驾照”。
4. 实战:怎么在 Java 里开线程?
在 Java 的进化史上,雇佣“厨师”主要有三种方式。搞清楚它们的区别,你才算真入门。
① 继承 Thread 类(老派写法)
这是最原始的方法,直接定义一个具体的厨师类。
java
// 定义一个"独行侠厨师",他就是一个线程
class MyChef extends Thread {
public void run() {
System.out.println("独行侠厨师在炒菜...");
}
}
public class Demo {
public static void main(String[] args) {
// 启动线程——注意是 start(),不是 run()!
new MyChef().start();
}
}
⚠️ 致命细节:一定要调
start()而不是run()。调run()就相当于你直接跑去炒菜,根本没请新厨师,程序还是单线程。
② 实现 Runnable 接口(主流做法)
这是目前最通用的方式:实现了“任务(菜谱)”与“执行者(厨师)”的分离。
java
// 1. 定义任务:我要干什么(菜谱)?
Runnable task = () -> {
System.out.println(Thread.currentThread().getName() + " 正在炸鸡...");
};
// 2. 雇佣员工并开始:让谁干?
new Thread(task, "厨师A").start();
这样做的好处是:菜谱(任务)可以被多个厨师复用,更灵活,也符合面向对象“组合优于继承”的原则。
③ 线程池 (Thread Pool) —— 职业选手的选择
资深警告:在真实项目中,严禁频繁手动创建和销毁线程!
java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
// 创建一个包含3个名额的"员工池"
ExecutorService pool = Executors.newFixedThreadPool(3);
// 往池子里不停扔任务,厨师们自动接活
pool.execute(() -> System.out.println("池子里的老师傅正在处理点餐..."));
pool.execute(() -> System.out.println("另一位师傅在处理外卖订单..."));
// 告知不再接收新任务,已有任务执行完后关闭
pool.shutdown();
线程池就是预先养好一批“厨师”,任务来了直接分配,任务干完了他们不回家,等着下一批任务。避免反复请人、辞退的巨大开销,还能防止任务太多把系统挤爆。
5. 资深对比:我该怎么选?
| 方式 | 角色比喻 | 优点 | 缺点 | 生产建议 |
|---|---|---|---|---|
| 继承 Thread | 孤儿厨师 | 写法最直观 | 限制继承,耦合度过高 | 了解即可,实际勿用 |
| 实现 Runnable | 通用菜谱 | 灵活、支持资源共享 | 仍需手动 new Thread,未享受池化能力 | 适合配合线程池使用 |
| 线程池 | 餐饮集团 | 性能最高,控人数,防崩溃 | 参数配置需要了解 | 企业级首选 |
简单总结一句:永远把任务写成 Runnable(或 Callable),然后用线程池去执行它。
6. 给新手的避坑指南
看完上面的内容你可能已经跃跃欲试,但请先读完这几条“血泪教训”,能帮你少熬几个夜:
-
坑1:把 run() 当成 start()
调run()只是普通方法调用,不会创建新线程,你的“新厨师”其实根本不存在。一定要用start()才能真正启动一个新线程。 -
坑2:一堆厨师抢一勺盐 —— 线程安全问题
当多个线程同时修改同一个变量(比如剩余食材数量),不加锁就会发生“两个厨师同时拿走最后一份鸡块”的惨剧,数据错乱。这时候必须用synchronized或Lock,保证同一时刻只有一个厨师能碰关键数据。 -
坑3:请来的厨师忘了解雇 —— 线程泄漏
创建了大量线程却从不关闭,就像请了100个临时工干完活不让他们走,系统资源被吃光,最终程序崩溃。线程池用完后一定记得shutdown()。 -
坑4:锁加多了,厨房彻底瘫痪 —— 死锁
厨师A拿着盐等油,厨师B拿着油等盐,谁也不肯先放手。避免死锁的关键是:规定好所有厨师永远按相同的顺序拿资源(比如先拿盐,再拿油),并且尽量缩小锁的范围。 -
坑5:以为线程越多越快 —— 上下文切换成本
100个厨师挤在一个灶台前,光互相撞来撞去、让路、交换工具的时间就比真正炒菜的时间还长。线程数最好设置为 CPU 核心数的 2 倍左右(视任务类型而定),线程池正好帮你控住数量。
总结一下:
多线程不是玄学,它只是让程序在同一时间内“分身”去处理不同的事。记住三个核心:
-
任务:你要干什么(菜谱)
-
执行者:让谁来干(厨师 / 线程池)
-
锁:什么时候只能一个人干(防抢盐)
掌握了这三样,你就已经跨过了多线程的大门!剩下的就是在实战中慢慢积累经验,希望各位能从这个学到新知识,祝我们无限进步!
更多推荐



所有评论(0)