[面试题] Java创建线程有几种方式?线程池 ThreadPoolExecutor 的具体参数有哪些?
📖 版块 A:知识讲解版 (温故知新)
Java 创建线程的方式
从不同维度看,创建方式有以下几种:
- 继承 Thread 类:重写 run() 方法。
- 实现 Runnable 接口:解耦任务与线程,支持多继承。
- 实现 Callable 接口:配合 FutureTask 使用,可以获取执行结果并处理异常。
- 线程池(ExecutorService):实际开发中最推荐的方式,通过预建线程减少频繁创建/销毁带来的开销。注:从底层源码看,其实只有一种方式,就是通过 new Thread().start() 来调用 native 方法启动内核线程。
ThreadPoolExecutor 的七大参数
- corePoolSize (核心线程数):常驻线程池的“正式员工”数,即使空闲也不会被回收。
- maximumPoolSize (最大线程数):池中允许存在的最大线程上限(核心+临时)。
- keepAliveTime (空闲线程存活时间):临时线程(非核心)空闲超过这个时间会被销毁。
- unit (时间单位):keepAliveTime 的单位(秒、毫秒等)。
- workQueue (任务队列):暂存等待执行任务的阻塞队列(如 LinkedBlockingQueue)。
- threadFactory (线程工厂):用于定制线程名、优先级等。
- handler (拒绝策略):当队列满且线程数达最大值时的处理方案(丢弃、抛异常、调用者运行等)。
🎤 版块 B:面试实战版 (高手逐字稿)
Q:Java 创建线程有几种方式?线程池 ThreadPoolExecutor 的具体参数有哪些?
🧠 破局思路:这是一道看似基础但容易拉开档次的题。回答“创建方式”时,高手会点出“任务定义与线程驱动”的区别,而不仅是罗列 API。回答“参数”时,要能结合线程池的工作流程,按照“逻辑链路”来背诵参数,而不是死记硬背,并能指出生产环境中避坑的细节。
💬 高手逐字稿:
关于 Java 创建线程的方式,在语法层面,我们常用的有四种:继承 Thread 类、实现 Runnable 接口、实现带返回值的 Callable 接口,以及通过线程池启动。但我认为,从本质上讲,Java 中创建线程的方式其实只有一种,那就是通过 new Thread().start() 启动。Runnable 和 Callable 只是定义的“任务内容”,并不是真正的“线程实体”。在实际工程中,为了避免频繁开关线程带来的抖动,我们几乎 100% 都会选择通过线程池来管理。
说到线程池,也就是 ThreadPoolExecutor,它一共有 7 个核心参数。我通常把它们理解为一个“外包公司”的运作逻辑:
首先是 corePoolSize,即核心线程数。这相当于公司的“正式员工”,一旦入职就不会轻易辞退。当正式员工忙不过来时,新任务会进入 workQueue(阻塞队列)排队。如果队列也塞满了,公司会招聘临时工,此时会扩充到 maximumPoolSize(最大线程数)。当任务量降下来后,这些临时工如果空闲时间超过了 keepAliveTime(及配套的 unit 时间单位),就会被解雇。而线程的具体创建逻辑是由 threadFactory 决定的,建议在生产环境中自定义工厂,给线程起个好辨认的名字。最后,如果正式员工、临时工都满了,队列也爆了,就会触发 handler 拒绝策略。Java 默认提供了四种策略:抛异常、丢弃、丢弃最旧的和由调用者自己执行。
💡 高手自问自答(深挖细节):
这里有一个容易被忽略的细节:核心线程数在初始状态下是不会启动的。也就是说,创建完线程池后,它的内部线程数是 0。只有当任务提交过来时,它才会逐一启动核心线程。如果想在池子创建后立刻把核心线程拉起来,可以手动调用 prestartAllCoreThreads() 方法。
另外,在生产环境中,我绝对不会使用 Executors 提供的静态工厂方法(如 newFixedThreadPool)。因为它们底层默认使用的阻塞队列长度是 Integer.MAX_VALUE,在高并发下会导致任务堆积,直接引发 OOM(内存溢出)。我们一般都会手动通过 ThreadPoolExecutor 的构造函数来明确定义这 7 个参数,以保证系统的稳定性。