【面试专栏|Java并发编程】从Runnable到Callable,Java4种线程创建方式


🍃 予枫个人主页
📚 个人专栏 : 《Java 从入门到起飞》《读研码农的干货日常》《Java 面试刷题指南

💻 Debug 这个世界,Return 更好的自己!


引言

线程是Java多线程编程的基础,也是面试高频考点。很多初学者只会用new Thread()创建线程,却不清楚还有其他方式,更分不清不同方式的优劣和适用场景。本文详解Java中创建线程的4种核心方式,结合代码示例、对比分析和面试官追问,帮你吃透线程创建,面试不踩坑、开发选对方案,建议点赞收藏备用~

文章目录

一、Java创建线程的4种核心方式(详解)

Java中创建线程主要有4种方式,分别是:继承Thread类、实现Runnable接口、实现Callable接口、使用线程池,每种方式各有特点,适用场景不同,我们逐一拆解。

1. 方式一:继承Thread类(基础入门)

这是最基础、最直观的创建线程方式,核心是继承Thread类,重写run()方法,调用start()方法启动线程。

核心原理

Thread类本身实现了Runnable接口,run()方法是线程的执行入口,start()方法会触发JVM调用run()方法,真正开启新线程(而非直接调用run(),直接调用只是普通方法调用,不会开启新线程)。

代码示例

java 复制代码
// 继承Thread类,重写run()方法
class MyThread extends Thread {
    @Override
    public void run() {
        // 线程执行逻辑
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + ":执行第" + (i+1) + "次");
            try {
                Thread.sleep(100); // 模拟线程执行耗时
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

// 测试类
public class ThreadTest {
    public static void main(String[] args) {
        // 创建线程实例
        MyThread thread1 = new MyThread();
        MyThread thread2 = new MyThread();
        // 启动线程
        thread1.start();
        thread2.start();
    }
}

优缺点

  • 优点:代码简单、直观,上手快,适合新手入门。
  • 缺点:Java单继承机制,继承Thread后无法再继承其他类,灵活性差;线程执行无返回值,无法捕获线程执行中的异常。

适用场景

适合简单的线程任务,无需返回值、无需继承其他类,比如简单的打印、测试等场景。

2. 方式二:实现Runnable接口(推荐基础用法)

为了解决单继承的局限性,Java提供了Runnable接口,核心是实现Runnable接口的run()方法,将线程逻辑封装在其中,再将Runnable实例传入Thread类启动线程。

核心原理

Runnable接口只有一个抽象方法run(),用于定义线程执行逻辑,通过Thread类的构造方法传入Runnable实例,启动线程时,Thread会调用Runnable的run()方法,实现线程与任务的解耦。

代码示例

java 复制代码
// 实现Runnable接口,重写run()方法
class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + ":执行第" + (i+1) + "次");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

// 测试类
public class RunnableTest {
    public static void main(String[] args) {
        // 创建Runnable实例(任务)
        MyRunnable runnable = new MyRunnable();
        // 将任务传入Thread,启动线程
        Thread thread1 = new Thread(runnable, "线程1");
        Thread thread2 = new Thread(runnable, "线程2");
        thread1.start();
        thread2.start();
    }
}

优缺点

  • 优点:避免单继承限制,可同时实现其他接口;线程与任务解耦,代码复用性强;多个线程可共享同一个Runnable实例(共享任务资源)。
  • 缺点:线程执行无返回值,无法直接捕获线程执行中的异常(需在run()方法内部处理)。

适用场景

最常用的基础场景,适合多线程共享同一个任务资源,无需返回值的情况,比如多线程执行同一个计算、打印任务等。

3. 方式三:实现Callable接口(有返回值场景)

Runnable接口的run()方法无返回值、无法抛异常,当我们需要线程执行完成后返回结果,或需要捕获线程执行中的异常时,就需要使用Callable接口。

核心原理

Callable接口有一个call()方法,该方法可返回值、可抛出异常;通过FutureTask类包装Callable实例,FutureTask实现了Runnable接口,可传入Thread类启动线程;最后通过FutureTask的get()方法获取线程执行结果(get()方法会阻塞,直到线程执行完成)。

代码示例

java 复制代码
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

// 实现Callable接口,指定返回值类型(此处为Integer)
class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= 5; i++) {
            sum += i;
            System.out.println(Thread.currentThread().getName() + ":计算i=" + i + ",当前和=" + sum);
            Thread.sleep(100);
        }
        return sum; // 返回线程执行结果
    }
}

// 测试类
public class CallableTest {
    public static void main(String[] args) throws Exception {
        // 创建Callable实例
        MyCallable callable = new MyCallable();
        // 用FutureTask包装Callable,用于获取返回值
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        // 启动线程
        Thread thread = new Thread(futureTask, "计算线程");
        thread.start();

        // 获取线程执行结果(get()方法会阻塞,直到线程执行完成)
        Integer result = futureTask.get();
        System.out.println("线程执行完成,最终和为:" + result);
    }
}

优缺点

  • 优点:线程执行有返回值,可抛出异常,便于异常处理;避免单继承限制,灵活性强。
  • 缺点:代码相对复杂,get()方法会阻塞主线程,需合理使用(避免主线程长期阻塞)。

适用场景

需要线程返回执行结果、需要处理线程异常的场景,比如多线程计算、数据查询后返回结果等。

4. 方式四:使用线程池(生产环境推荐)

以上3种方式都是手动创建线程,频繁创建和销毁线程会消耗大量系统资源,降低性能(线程是重量级资源)。生产环境中,推荐使用线程池管理线程,实现线程复用,提高效率。

核心原理

线程池是预先创建一定数量的线程,放入线程池中,当有任务时,从池中取出线程执行任务,任务完成后,线程不销毁,放回池中供下次使用;通过线程池可控制线程数量、管理线程生命周期,避免资源浪费。

代码示例(使用Executors工具类创建线程池)

java 复制代码
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

// 任务类(实现Runnable接口)
class MyTask implements Runnable {
    private String taskName;

    public MyTask(String taskName) {
        this.taskName = taskName;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + ":执行任务" + taskName);
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ":任务" + taskName + "执行完成");
    }
}

// 测试类
public class ThreadPoolTest {
    public static void main(String[] args) {
        // 1. 创建线程池(固定线程数为3)
        ExecutorService executorService = Executors.newFixedThreadPool(3);

        // 2. 提交任务(共5个任务,线程池3个线程复用)
        for (int i = 1; i <= 5; i++) {
            executorService.submit(new MyTask("Task" + i));
        }

        // 3. 关闭线程池(不再接收新任务,等待现有任务执行完成)
        executorService.shutdown();
    }
}

补充说明

Java中线程池核心接口是ExecutorService,常用实现类有ThreadPoolExecutor(推荐手动创建,可灵活配置参数)、ScheduledThreadPoolExecutor等;Executors工具类提供了快速创建线程池的方法(如newFixedThreadPool、newCachedThreadPool),但生产环境中不推荐使用Executors(存在资源耗尽风险),建议手动创建ThreadPoolExecutor。

优缺点

  • 优点:线程复用,减少线程创建/销毁的资源消耗;控制线程数量,避免线程过多导致系统卡顿;便于管理线程,可设置超时、拒绝策略等。
  • 缺点:配置相对复杂(手动创建ThreadPoolExecutor时);需合理设置线程池参数(核心线程数、最大线程数等),否则会影响性能。

适用场景

生产环境首选,适合大量、重复的线程任务,比如Web开发中处理请求、批量处理数据等场景。

二、4种方式对比分析(一目了然)

为了方便大家快速区分和选择,整理了以下对比表格,涵盖核心维度:

创建方式 实现方式 是否有返回值 是否可抛异常 灵活性 线程复用 适用场景
继承Thread类 继承Thread,重写run() 不可抛(需内部处理) 低(单继承限制) 简单测试、无需返回值
实现Runnable接口 实现Runnable,重写run() 不可抛(需内部处理) 高(可多实现) 多线程共享任务、无需返回值
实现Callable接口 实现Callable,重写call() 可抛异常 高(可多实现) 需要返回值、需处理异常
使用线程池 线程池管理线程,提交任务(Runnable/Callable) 可选(提交Callable有返回值) 可抛异常 生产环境、大量重复任务

核心总结

  • 新手入门:先掌握「继承Thread类」和「实现Runnable接口」,理解线程创建的基本原理。
  • 有返回值/异常处理:用「实现Callable接口」,配合FutureTask获取返回值。
  • 生产环境:优先用「线程池」,实现线程复用,提升性能和可维护性。

三、面试官追问环节(实战必备)

这部分是面试高频考点,比纯八股文更实用,帮你提前准备面试官的连环追问,建议重点记忆!

追问1:继承Thread类和实现Runnable接口的本质区别是什么?

核心区别有2点:

  1. 继承Thread类:线程与任务绑定(Thread类本身是线程,run()方法是任务),无法复用线程,且受单继承限制。
  2. 实现Runnable接口:线程与任务解耦(Runnable是任务,Thread是线程载体),可多个线程共享一个任务,无单继承限制,灵活性更高。
    补充:实际开发中,优先选实现Runnable接口,避免单继承的局限性。

追问2:Callable和Runnable的核心差异是什么?

最核心的3点差异:

  1. 方法不同:Runnable是run()方法,Callable是call()方法。
  2. 返回值:run()无返回值,call()有返回值(泛型指定)。
  3. 异常:run()不能抛出checked异常(只能内部try-catch),call()可以抛出checked异常,且能被外部捕获。

追问3:为什么生产环境不推荐使用Executors创建线程池?

Executors创建线程池存在以下风险,可能导致资源耗尽:

  1. newCachedThreadPool:无最大线程数限制,当任务量激增时,会创建大量线程,导致OOM(内存溢出)。
  2. newFixedThreadPool/newSingleThreadExecutor:队列是无界队列(LinkedBlockingQueue),当任务过多时,队列会无限扩容,导致OOM。
    推荐做法:手动创建ThreadPoolExecutor,指定核心线程数、最大线程数、阻塞队列、拒绝策略,灵活控制资源。

追问4:线程池的核心参数有哪些?(延伸追问,高频)

ThreadPoolExecutor的核心构造参数(5个):

  1. corePoolSize:核心线程数(线程池中长期存活的线程数)。
  2. maximumPoolSize:最大线程数(线程池中允许存在的最大线程数)。
  3. keepAliveTime:空闲线程存活时间(核心线程外的线程,空闲超过该时间会被销毁)。
  4. unit:keepAliveTime的时间单位(如TimeUnit.SECONDS)。
  5. workQueue:阻塞队列(用于存放等待执行的任务)。
    补充:还有2个可选参数(线程工厂、拒绝策略),拒绝策略用于处理任务过多时的拒绝逻辑(如AbortPolicy:直接抛出异常)。

四、总结

本文详解了Java创建线程的4种核心方式,从基础的继承Thread、实现Runnable,到有返回值的Callable,再到生产环境推荐的线程池,结合代码示例、对比分析和面试官追问,帮你全面掌握线程创建的知识点。

核心要点:

  1. 避免单继承,优先用Runnable接口;
  2. 有返回值/异常处理,用Callable+FutureTask;
  3. 生产环境,必用线程池(手动创建ThreadPoolExecutor);
  4. 面试重点掌握4种方式的区别、适用场景,以及线程池相关追问。

📌 本文已收录至我的Java多线程系列专栏,关注我,后续持续更新多线程进阶知识点(线程同步、线程安全、线程池实战等)。

💡 如果你有其他线程相关的疑问,或者想了解某部分的细节,欢迎在评论区留言讨论~

👍 觉得有用的话,点赞+收藏,避免下次找不到!

相关推荐
意疏1 天前
openJiuwen实战:用AsyncCallbackFramework为Agent增强器添加可观测性
java·服务器·前端
马士兵教育1 天前
2026年IT行业基本预测!计算机专业学生就业编程语言Java/C/C++/Python该如何选择?
java·开发语言·c++·人工智能·python·面试·职场和发展
Book思议-1 天前
顺序表和链表核心差异与优缺点详解
java·数据结构·链表
小杨的博客1 天前
Java + Selenium实现浏览器打印功能
java·selenium
wefly20171 天前
M3U8 播放调试天花板!m3u8live.cn纯网页无广告,音视频开发效率直接拉满
java·前端·javascript·python·音视频
兆子龙1 天前
antd 组件也做了同款效果!深入源码看设计模式在前端组件库的应用
java·前端·架构
祁梦1 天前
Redis从入门到入土 --- 黑马点评判断秒杀资格
java·后端
兆子龙1 天前
lodash 到 lodash-es 多的不仅仅是后缀!深入源码看 ES Module 带来的性能与体积优化
java·前端·架构
Memory_荒年1 天前
限流算法:当你的系统变成“网红景点”,如何避免被游客挤垮?
java·后端
我命由我123451 天前
Git 问题:Author identity unknown*** Please tell me who you are.
java·服务器·git·后端·学习·java-ee·学习方法