🏇 Thread 与 Runnable:“​​包子铺创业记​​”的故事

103 阅读5分钟

用一个“​​包子铺创业记​​”的故事,结合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?​

  1. 避免频繁创建/销毁Thread(性能差)

  2. 方便切换执行载体(线程池、HandlerThread等)


💎 ​​终极总结:一张图看懂选择策略​

image.png

​口诀​​:
​共享数据选Runnable,独享资源可Thread;
内存复用是王道,线程池里藏乾坤!​
​ 🚀