用一个“包子铺创业记”的故事,结合Java源码和实际开发场景,为你彻底讲透Thread和Runnable的区别!保证小白也能秒懂!
🎭 故事角色设定
- 老板(你):主线程(Main Thread),负责统筹全局
- 传统学徒(Thread):每个学徒独立拥有一套做包子的工具和原料
- 现代流水线(Runnable):所有工人共享一套中央厨房设备和原料池
- 包子任务:多线程要执行的任务(
run()方法里的代码)
🧩 第一章:创业初期的传统模式(继承Thread)
java
Copy
// 传统学徒模式:每个学徒独立工作
class OldApprentice extends Thread {
// **关键区别1:每个Thread对象独占资源**
private int flour = 5; // 每个学徒自己有5斤面粉
@Override
public void run() {
while (flour > 0) {
System.out.println(getName() + ":做出1个包子,剩余面粉" + --flour + "斤");
}
}
}
// 老板在主线程开店
public class Boss {
public static void main(String[] args) {
// 招了3个传统学徒
new OldApprentice().start(); // 学徒1
new OldApprentice().start(); // 学徒2
new OldApprentice().start(); // 学徒3
}
}
📝 运行结果:
Copy
Thread-0:做出1个包子,剩余面粉4斤
Thread-0:做出1个包子,剩余面粉3斤
...(学徒1独自用完5斤面粉)
Thread-1:做出1个包子,剩余面粉4斤
...(学徒2独自用完5斤面粉)
Thread-2:做出1个包子,剩余面粉4斤
...(学徒3独自用完5斤面粉)
🔍 核心问题(源码视角):
每个
Thread子类实例独立拥有自己的flour变量。
相当于开了3家独立小店,每家都备了5斤面,共消耗15斤面,但只产出15个包子(效率低下) 。
🚀 第二章:升级现代工厂模式(实现Runnable)
java
Copy
// 现代流水线:工人共享中央厨房
class ModernWorker implements Runnable {
// **关键区别2:所有线程共享同一份资源!**
private int flour = 5; // 面粉放在中央仓库
@Override
public void run() {
while (flour > 0) {
// **隐患:多线程同时操作共享变量会翻车!**
System.out.println(Thread.currentThread().getName() + ":抢到1斤面粉,剩余" + --flour + "斤");
}
}
}
public class Boss {
public static void main(String[] args) {
// 创建1个共享任务(中央厨房)
Runnable task = new ModernWorker();
// 招3个工人操作同一条流水线
new Thread(task, "工人A").start();
new Thread(task, "工人B").start();
new Thread(task, "工人C").start();
}
}
📝 运行结果(灾难现场):
Copy
工人A:抢到1斤面粉,剩余4斤
工人B:抢到1斤面粉,剩余3斤
工人C:抢到1斤面粉,剩余2斤
工人A:抢到1斤面粉,剩余0斤
工人B:抢到1斤面粉,剩余-1斤 // 面粉负数!重大生产事故!
🔍 核心问题:
多个线程同时读写共享变量
flour,导致数据竞争(Data Race)。
相当于3个工人抢同一个面缸,最后把面缸掏穿了 。
🛡️ 第三章:老板的救星——同步锁(synchronized)
java
Copy
class ModernWorker implements Runnable {
private int flour = 5;
@Override
public void run() {
while (true) {
// 给共享资源加锁(面缸上锁)
synchronized (this) {
if (flour <= 0) break;
System.out.println(Thread.currentThread().getName() + ":安全取出1斤面粉,剩余" + --flour + "斤");
}
// 模拟其他操作(比如包包子)
try { Thread.sleep(100); } catch (Exception e) {}
}
}
}
📝 运行结果(秩序井然):
Copy
工人A:安全取出1斤面粉,剩余4斤
工人B:安全取出1斤面粉,剩余3斤
工人C:安全取出1斤面粉,剩余2斤
工人A:安全取出1斤面粉,剩余1斤
工人B:安全取出1斤面粉,剩余0斤 // 正常停止,没有负数!
💡 关键改进:
通过
synchronized锁确保同一时间只有一个线程操作共享资源,避免数据错乱
📊 第四章:Thread vs Runnable 核心区别总结
| 维度 | Thread(传统学徒) | Runnable(现代流水线) | 支持证据 |
|---|---|---|---|
| 资源占用 | 每个线程独立资源,内存消耗大 | 多个线程共享同一任务资源,内存高效 | 45 |
| 灵活性 | 继承Thread后无法继承其他类(Java单继承限制) | 可实现多个接口,自由扩展功能 | 23 |
| 适用场景 | 线程需独立操作不共享的资源 | 绝大多数场景!尤其需共享数据时(如Android网络请求) | 810 |
| 代码复用 | 相同任务需创建多个Thread子类 | 一个Runnable可被多个Thread复用 | 37 |
| 线程控制 | 可直接调用interrupt(), join()等方法 | 需通过持有Thread对象间接控制 | 16 |
🧠 第五章:从源码看本质(JDK源码揭秘)
1️⃣ Thread类源码(精简版):
java
Copy
public class Thread implements Runnable {
private Runnable target; // 关键!Thread实际是Runnable的包装壳
// 构造方法:可传入Runnable任务
public Thread(Runnable target) {
this.target = target;
}
@Override
public void run() {
if (target != null) {
target.run(); // **实际执行的是Runnable的run()!**
}
}
}
真相:Thread自己也是
Runnable的实现类,只是多了一层包装!
2️⃣ 为什么推荐Runnable?
-
解耦:任务逻辑(Runnable)和线程管理(Thread)分离
-
复用:同一个Runnable可提交给线程池、定时器等
-
Lambda支持:代码更简洁(Java 8+)
java Copy // Runnable的Lambda写法(无需显式类!) new Thread(() -> System.out.println("Lambda包子任务")).start();
🚨 第六章:Android开发中的血泪教训
案例:多线程下载图片(错误示范)
java
Copy
// ❌ 错误:继承Thread导致无法复用
class DownloadThread extends Thread {
private String url;
public DownloadThread(String url) { this.url = url; }
@Override
public void run() {
// 下载逻辑...
}
}
// 每次下载都new新线程(内存爆炸!)
new DownloadThread("url1").start();
new DownloadThread("url2").start();
✅ 正确姿势:Runnable + 线程池
java
Copy
// ✅ 正解:Runnable + 线程池
ExecutorService pool = Executors.newCachedThreadPool();
Runnable downloadTask = () -> {
// 下载逻辑(可共享网络工具类)
};
pool.execute(downloadTask); // 提交到线程池复用
为什么Android官方推荐Runnable?
避免频繁创建/销毁Thread(性能差)
方便切换执行载体(线程池、HandlerThread等)
💎 终极总结:一张图看懂选择策略
口诀:
共享数据选Runnable,独享资源可Thread;
内存复用是王道,线程池里藏乾坤! 🚀