Java 线程与多线程:从实战到避坑,一篇彻底打通任督二脉
Java 线程与多线程:从实战到避坑,一篇彻底打通任督二脉
多线程是 Java 高并发编程的核心基石。用好了,系统如虎添翼;用错了,轻则死锁崩溃,重则内存溢出、生产事故。本文从实战出发,带你从入门到精通,再到避坑,一站式搞定。
一、先把地基打牢:核心概念厘清
| 概念 | 一句话定义 | 类比 |
|---|---|---|
| 进程 | 操作系统资源分配的最小单位,拥有独立内存空间 | 一个自给自足的工厂 |
| 线程 | CPU 调度的最小单位,共享进程资源 | 工厂里的工人 |
| 并发 | 单 CPU 上多任务交替执行(时间片轮转) | 一个厨师快速切换炒三道菜 |
| 并行 | 多 CPU 上多任务同时执行 | 三个厨师同时炒三道菜 |
关键认知:Java 线程采用 1:1 映射模型,每个 Java 线程直接对应一个操作系统原生线程。线程创建、切换的开销远小于进程,这正是多线程比多进程更高效的根本原因。
二、四种创建线程的方式:从青铜到王者
🥇 方式一:继承 Thread 类(青铜级)
java
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
// 启动
new MyThread("线程A").start();
⚠️ 致命缺陷:Java 单继承,继承了 Thread 就不能再继承其他类,扩展性极差。不推荐在生产环境使用。
🥈 方式二:实现 Runnable 接口(黄金级·最推荐)
java
class MyRunnable implements Runnable {
private final String name;
public MyRunnable(String name) {
this.name = name;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(name + " 执行: " + i);
}
}
}
// 启动 —— 解耦了线程逻辑与线程对象本身
new Thread(new MyRunnable("线程A"), "窗口1").start();
new Thread(new MyRunnable("线程B"), "窗口2").start();
优势:避免单继承限制,同一个 Runnable 实例可被多个线程共享,是日常开发的主流写法。
🥉 方式三:实现 Callable + FutureTask(钻石级)
java
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= 100; i++) sum += i;
return sum;
}
}
// 启动并获取结果
FutureTask<Integer> futureTask = new FutureTask<>(new MyCallable());
new Thread(futureTask).start();
Integer result = futureTask.get(); // 阻塞直到计算完成
System.out.println("结果: " + result);
核心价值:支持返回值和抛出异常,弥补了 Runnable 的短板。
| 方式 | 返回值 | 抛异常 | 推荐场景 |
|---|---|---|---|
| Runnable | ❌ | ❌ | 火了就忘型任务 |
| Callable | ✅ | ✅ | 需要结果的计算任务 |
👑 方式四:线程池(王者级·生产必用)
java
ExecutorService executor = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
final int taskId = i;
executor.execute(() -> {
System.out.println(Thread.currentThread().getName()
+ " 执行任务 " + taskId);
});
}
executor.shutdown();
executor.awaitTermination(60, TimeUnit.SECONDS);
为什么线程池是王道?
| 优势 | 说明 |
|---|---|
| 降低资源消耗 | 复用已创建线程,避免反复创建销毁 |
| 提高响应速度 | 任务到达时线程已就绪 |
| 防止资源耗尽 | 限制线程数量,避免无节制创建 |
| 统一管理监控 | 生命周期可控,便于排查问题 |
🚨 阿里巴巴开发手册明确规定:禁止使用 Executors 创建线程池! 必须用
ThreadPoolExecutor构造器,因为Executors.newFixedThreadPool()使用的是无界队列,任务激增时会导致 OOM。
java
// ✅ 推荐写法
ThreadPoolExecutor pool = new ThreadPoolExecutor(
5, // 核心线程数
10, // 最大线程数
60L, TimeUnit.SECONDS, // 空闲线程存活时间
new LinkedBlockingQueue<>(100), // 有界队列!
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
三、线程的六种状态:一张图看透生死
| 状态 | 含义 | 典型触发 |
|---|---|---|
| NEW | 新建,尚未 start() | new Thread() |
| RUNNABLE | 就绪/运行中,等待 CPU 调度 | start() 之后 |
| BLOCKED | 等待获取监视器锁 | synchronized 竞争失败 |
| WAITING | 无限期等待 | object.wait() / join() |
| TIMED_WAITING | 限时等待 | sleep() / wait(timeout) |
| TERMINATED | 执行完毕,已死亡 | run() 执行完 / 异常终止 |
四、线程同步:不加锁,就是在裸奔
🔥 经典问题:100 人抢 10 张票
java
class TicketService {
private int ticket = 10;
// ❌ 非线程安全 —— 会超卖!
public void sell() {
if (ticket > 0) {
try { Thread.sleep(300); } catch (Exception e) {}
System.out.println("卖出: " + ticket--);
}
}
}
✅ 方案一:synchronized 同步方法
java
public synchronized void sell() {
if (ticket > 0) {
try { Thread.sleep(300); } catch (Exception e) {}
System.out.println("卖出: " + ticket--);
}
}
✅ 方案二:synchronized 代码块(更精细)
java
public void sell() {
synchronized (this) { // 只锁核心逻辑,性能更优
if (ticket > 0) {
try { Thread.sleep(300); } catch (Exception e) {}
System.out.println("卖出: " + ticket--);
}
}
}
✅ 方案三:ReentrantLock(高并发推荐)
java
private final ReentrantLock lock = new ReentrantLock();
public void sell() {
lock.lock();
try {
if (ticket > 0) {
try { Thread.sleep(300); } catch (Exception e) {}
System.out.println("卖出: " + ticket--);
}
} finally {
lock.unlock(); // ⚠️ 必须在 finally 中释放!
}
}
| 方案 | 适用场景 |
|---|---|
| synchronized | 简单场景,代码简洁 |
| ReentrantLock | 高并发、需要尝试锁/超时锁 |
| AtomicInteger | 简单计数(如 count++) |
⚠️ volatile 只能保证可见性,不能保证原子性!
count++是读-改-写三步操作,必须用AtomicInteger。
五、五大深坑:踩一个就够你喝一壶
🕳️ 坑一:线程池参数不当 → OOM
Executors.newFixedThreadPool() 使用无界队列,任务堆积时内存直接爆炸。
解法:用有界队列 + 拒绝策略(CallerRunsPolicy 适合限流降级)。
🕳️ 坑二:ThreadLocal 内存泄漏
java
// ❌ 忘记清理
threadLocal.set(value);
// 线程被回收后,value 永远驻留内存!
// ✅ 必须在 finally 中清除
try {
threadLocal.set(value);
// ... 业务逻辑
} finally {
threadLocal.remove();
}
🕳️ 坑三:死锁 —— 程序假死,无声无息
java
// ❌ 经典死锁:A 锁 X 等 Y,B 锁 Y 等 X
Object lockA = new Object();
Object lockB = new Object();
Thread t1 = new Thread(() -> {
synchronized (lockA) {
synchronized (lockB) { /* ... */ }
}
});
Thread t2 = new Thread(() -> {
synchronized (lockB) { // 顺序反了!
synchronized (lockA) { /* ... */ }
}
});
解法:
- 固定加锁顺序(所有线程按同一顺序获取锁)
- 使用
tryLock(timeout)设置超时 - 用
jstack定位死锁线程
🕳️ 坑四:吞掉 InterruptedException
java
// ❌ 最常见的错误
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace(); // 吞掉了!线程中断状态被清除
}
// ✅ 正确做法:恢复中断状态
catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 恢复中断标志
throw new RuntimeException(e);
}
🕳️ 坑五:误以为 start() == run()
java
MyThread t = new MyThread();
t.run(); // ❌ 这只是普通方法调用!不会启动新线程
t.start(); // ✅ 这才是真正启动新线程
一个线程对象只能 start() 一次,第二次调用会抛出
IllegalThreadStateException。
六、调试神器:jstack 抓线程快照
当线上出现 CPU 飙高、程序假死时:
bash
# 1. 找到 Java 进程 PID
jps -l
# 2. 生成线程快照(重点看 BLOCKED 状态)
jstack -l 12345 > thread_dump.txt
重点关注:
java.lang.Thread.State: BLOCKED→ 锁竞争waiting to lock <0x...>→ 正在等哪把锁- 死锁信息会明确标出
Found one Java-level deadlock
| JVM | WAITING 状态显示 |
|---|---|
| HotSpot | WAITING (on object monitor) |
| IBM J9 | Waiting for monitor entry |
七、最佳实践清单(建议收藏)
| ✅ 要做 | ❌ 别做 |
|---|---|
| 用线程池,别手动 new Thread | 用 Executors 创建线程池 |
| 有界队列 + 拒绝策略 | 无界队列赌不会满 |
finally 中释放锁和资源 |
在 try 里释放锁 |
volatile 保可见,Atomic 保原子 |
用 volatile 替代原子类 |
join() / CountDownLatch 做线程协作 |
wait() 不加循环条件判断 |
CompletableFuture 替代回调地狱 |
嵌套 Future.get() 阻塞主线程 |
用 jstack/VisualVM 排查问题 |
靠 System.out.println 调多线程 |
写在最后
多线程编程的本质是在"充分利用资源"和"保证数据安全"之间走钢丝。它不是银弹,而是一把双刃剑——用对了,系统吞吐量翻倍;用错了,死锁、OOM、数据错乱接踵而至。
记住一句话:对多线程保持敬畏之心。 在代码评审时紧盯同步边界,在生产环境做好线程池监控,在测试阶段用并发测试工具系统性验证。只有经过充分验证的代码,才配得上生产环境的信任。
🧠 终极建议:能不用多线程就别用,能用线程池就别手写 Thread,能用
CompletableFuture就别用Future.get()阻塞。简单,才是最高级的并发。
更多推荐

所有评论(0)