本文是 Java 面向对象程序设计课程的课后作业,详细讲解 Java 中两种最基础的线程创建方式:继承 Thread 类实现 Runnable 接口。通过代码示例、经典售票案例和多维度对比,深入分析两者的本质区别、各自的优缺点以及实际开发中的选择原则。


一、Java 创建线程的两种基础方式

在 Java 中,创建线程最常用的有两种方式:

  1. 继承 java.lang.Thread,重写 run() 方法
  2. 实现 java.lang.Runnable 接口,实现 run() 方法,再将其作为参数传给 Thread 对象

很多初学者会疑惑:两种方式都能创建线程,它们到底有什么区别?为什么老师总说推荐使用 Runnable 接口?下面我们通过代码和案例逐一分析。


二、两种方式的代码实现

2.1 方式一:继承 Thread 类

实现步骤

  1. 定义一个类继承 Thread
  2. 重写 run() 方法,编写线程执行的任务逻辑
  3. 创建该子类的实例对象
  4. 调用 start() 方法启动线程
/**
 * 方式一:继承Thread类创建线程
 */
public class MyThread extends Thread {

    // 构造方法:设置线程名
    public MyThread(String name) {
        super(name);
    }

    // 重写run方法,编写线程任务
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " 执行:" + i);
            try {
                // 模拟线程执行耗时
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    // 测试
    public static void main(String[] args) {
        // 创建两个线程对象
        MyThread t1 = new MyThread("线程A");
        MyThread t2 = new MyThread("线程B");

        // 启动线程(注意:调用start()才会启动新线程,调用run()只是普通方法调用)
        t1.start();
        t2.start();
    }
}

2.2 方式二:实现 Runnable 接口

实现步骤

  1. 定义一个类实现 Runnable 接口
  2. 实现 run() 方法,编写线程执行的任务逻辑
  3. 创建该实现类的实例
  4. 将实例作为参数传入 Thread 的构造方法,创建 Thread 对象
  5. 调用 Thread 对象的 start() 方法启动线程
/**
 * 方式二:实现 Runnable 接口创建线程
 */
public class MyRunnable implements Runnable {

    // 实现run方法,编写线程任务
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " 执行:" + i);
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    // 测试
    public static void main(String[] args) {
        // 创建任务对象
        MyRunnable task = new MyRunnable();

        // 将任务对象传给Thread,创建多个线程执行同一个任务
        Thread t1 = new Thread(task, "线程A");
        Thread t2 = new Thread(task, "线程B");

        t1.start();
        t2.start();
    }
}

注意:Runnable 接口只有一个 run() 方法,它本身不具备启动线程的能力,最终还是要靠 Thread 类的 start() 方法来启动线程。Runnable 的作用是封装线程要执行的任务


三、核心区别多维度对比

3.1 对比总表

对比维度 继承 Thread 类 实现 Runnable 接口
本质 继承一个线程类 实现一个任务接口
单继承限制 受 Java 单继承限制,子类不能再继承其他类 不受单继承限制,还可以同时继承其他类、实现其他接口
资源共享 多个线程间难以共享资源,需借助静态变量 天然支持多个线程共享同一个任务对象中的资源
代码耦合度 线程对象和任务逻辑绑定在一起,耦合度高 任务逻辑与线程对象分离,解耦性好
代码复用性 任务只能被该线程类使用,复用性差 任务可以被多种线程、线程池复用,复用性强
线程池支持 不支持直接放入线程池执行 完美兼容线程池,是实际开发的标准用法
适用场景 简单的单线程场景、无需共享资源的场景 多线程共享资源、复杂业务、实际项目开发

3.2 详细解读

1. 单继承限制(最本质区别)

Java 是单继承语言,一个类只能有一个直接父类。如果选择继承 Thread 类,那么这个类就不能再继承其他任何类了,这会大大限制类的扩展性。

而实现 Runnable 接口完全没有这个问题,一个类可以同时实现多个接口,还可以继承其他父类,灵活性更高。

2. 资源共享能力

这是两者最直观的区别。实现 Runnable 接口可以很方便地让多个线程共享同一份资源,而继承 Thread 类做不到这一点。

我们用经典的“多窗口售票”案例来演示:

案例需求:3 个窗口同时卖 100 张票,总票数是共享的。

❌ 继承 Thread 实现(需要用静态变量共享)
public class TicketThread extends Thread {
    // 必须用 static 静态变量,才能让多个线程对象共享
    private static int tickets = 100;

    public TicketThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        while (tickets > 0) {
            System.out.println(getName() + " 卖出第 " + tickets-- + " 张票");
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        // 每个线程对象都有自己的成员变量,必须靠 static 共享
        new TicketThread("窗口1").start();
        new TicketThread("窗口2").start();
        new TicketThread("窗口3").start();
    }
}
✅ 实现 Runnable(天然共享)
public class TicketRunnable implements Runnable {
    // 普通成员变量即可,多个线程共享同一个对象
    private int tickets = 100;

    @Override
    public void run() {
        while (tickets > 0) {
            System.out.println(Thread.currentThread().getName() + " 卖出第 " + tickets-- + " 张票");
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        // 只创建一个任务对象,交给3个线程执行
        TicketRunnable task = new TicketRunnable();
        new Thread(task, "窗口1").start();
        new Thread(task, "窗口2").start();
        new Thread(task, "窗口3").start();
    }
}

对比结论

  • 继承 Thread 的方式必须借助 static 静态变量才能共享资源,而静态变量生命周期长,容易带来其他问题
  • 实现 Runnable 的方式只需要创建一个任务对象,多个线程共享该对象的成员变量,更加自然合理
3. 代码解耦与复用
  • 继承 Thread:线程对象和任务逻辑绑定死了,一个线程类只能做一件事,复用性差
  • 实现 Runnable:把「线程要执行的任务」单独封装成一个对象,和线程本身解耦。同一个任务可以交给不同的线程执行,也可以交给线程池执行,复用性极强
4. 与线程池的兼容性

在实际企业开发中,我们几乎不会手动 new Thread,而是使用线程池来管理线程。而线程池只能接收 RunnableCallable 类型的任务,不能直接接收 Thread 子类。

从这一点来说,实现 Runnable 接口是工业界的标准做法,而继承 Thread 更多只出现在入门教学中。


四、开发中如何选择?

推荐原则

  1. 优先选择实现 Runnable 接口

    这是绝大多数场景下的最佳选择,尤其是在需要多线程共享资源、需要良好扩展性、或者配合线程池使用的场景。

  2. 什么时候可以用继承 Thread?

    • 非常简单的单线程场景,只是为了快速测试
    • 该类本身就是一个线程实体,不需要再继承其他类
    • 教学演示、入门练习

行业共识

在真实的 Java 后端开发中,99% 的场景都会使用 Runnable(或 Callable)来定义线程任务,配合线程池来执行。直接继承 Thread 类创建线程的写法几乎不会出现在生产代码中,因为它的扩展性差、资源浪费严重,也不利于线程管理。


五、学习总结

通过这次作业,我搞懂了继承 Thread 和实现 Runnable 这两种创建线程方式的区别。以前只知道两种写法都能运行线程,不知道背后的设计思想和适用场景差异这么大。

我最大的收获有两点:

  1. 面向接口编程的优势Runnable 接口完美体现了面向接口编程的思想,它把「线程执行的任务」和「线程本身」分离开,带来了更好的扩展性、复用性和解耦性。
  2. 资源共享的设计差异:通过售票案例的对比,我直观地感受到了两种方式在资源共享上的区别,也理解了为什么实际开发都推荐用 Runnable

如果觉得这篇文章对你有帮助,欢迎点赞、收藏、关注!有任何问题可以在评论区留言交流~

更多推荐