“你知道 Executor 和 Executors 的区别吗?”
这句话,是我在一次 Java 社招面试时,被面试官笑着抛出来的第一道题。那一刻,我脑子里虽然有印象,但也没准备好“完美回答”。今天,作为一个从社招“坑坑洼洼”中走过来的老程序员,我想用一个轻松的小故事,和你聊聊这道经典的面试题——希望你看完后,再遇到这题,能胸有成竹地笑着说:“这个,我熟。”
故事开场:那年,我去面试
时间回到两年前。那时候的我,已经在一家中型互联网公司干了五年。代码写了不少,也踩了不少坑。可当真正要面试大厂的时候,我才发现,自己对很多基础知识,理解得还不够深入。
那次社招面试的场景我到现在还记得清清楚楚:
面试官微笑着问:“小米,线程池你应该用过吧?”
我点点头:“嗯,用得不少,Executors.newFixedThreadPool(5)我天天用。”
面试官点点头:“那你知道Executor和Executors有什么区别吗?”
我脑子一紧:这……不是差一个s的事吗?
我一边嘴硬地说“Executor 是接口,Executors 是工具类”,一边心虚地知道,这回答——太浅了。
面试官没有直接否定我,而是笑了笑说:“我们来深入聊聊。”
那天我被“教育”了四十分钟,整整四十分钟。
后来,我回家做了三天的笔记,写了两篇总结,才终于把这对“线程池的亲兄弟”搞明白。
今天,我就用讲故事的方式,把我当年那“被教育”的知识点,跟你掰开揉碎说一遍。
主角登场:Executor 和 Executors
我们来想象一个场景:
你是一位餐厅经理(Main 线程),你要安排服务员(线程)去处理顾客订单(任务)。你不能什么都亲力亲为,你需要一个调度系统。
于是你请来了两个角色:
- Executor(执行者) :他是一个接口,你可以把任务交给他,由他决定怎么执行。
- Executors(执行者们) :他是一个工厂类,可以帮你创建各种不同风格的“Executor”。
- Executor 是接口,定义了“线程池”的标准。
- Executors 是工具类,提供了静态方法来创建不同类型的线程池。
这个时候你可能会想:哦,不就是一个定义规则,一个提供实例吗?差不多该结束了吧?
不,小米摇摇头——刚刚开始。
再深入一层:Executor 的真面目
先来看 Executor 这个接口的定义:
只有一个方法,execute(Runnable command),听起来好像就是线程池的核心。
没错,它是 Java 并发包(java.util.concurrent)中的基石。任何想要被称为“执行者”的类,只要实现这个接口,就能被拿来“跑任务”。
典型的实现有:
- ThreadPoolExecutor: 这个是真正干活的线程池类。
- ScheduledThreadPoolExecutor: 支持定时任务和周期任务的线程池。
Executor 提供了“面向接口编程”的基础,但他本人并不关心你怎么实现。你想直接 new 一个 Thread?OK;你想用一个线程池去维护线程复用?也OK。
Executors 的“糖衣炮弹”
再来看 Executors,这是一个工具类,里面提供了大量创建线程池的静态方法:
这些方法很方便,随手就能搞出一个线程池用来执行任务。
那你可能会想:这么方便,我为什么还要研究别的方式?
别急,问题就来了。
Executors 的“坑”你踩过吗?
你知道吗?有些大厂的代码规范明确禁止使用 Executors 工厂方法创建线程池。
为什么?
我们来一一拆解:
1. Executors.newFixedThreadPool(int n)
- 优点:固定线程数,避免线程数量无限制增长。
- 缺点:任务队列是无界队列(LinkedBlockingQueue) ,如果任务提交过多,可能导致OOM(内存溢出)。
2. Executors.newCachedThreadPool()
- 优点:线程数不固定,任务多就创建线程。
- 缺点:线程数无限制增长,高并发情况下可能直接把系统拖垮。
3. Executors.newSingleThreadExecutor()
- 优点:只有一个线程,任务按顺序执行,适合一些“串行化任务”。
- 缺点:同样使用无界队列,如果任务堆积,也可能OOM。
4. Executors.newScheduledThreadPool()
- 用于定时或周期性任务。
- 本身还算稳定,但使用不当一样会出问题。
面试官真正想听的答案
现在,我们再回到那个面试现场。
如果你只是回答“Executor 是接口,Executors 是工具类”,面试官可能会点头,但心里已经给你贴了个“只懂 API,不懂设计”的标签。
正确的答题姿势是这样的:
“Executor 是一个定义了任务执行机制的接口,主要方法是 execute(Runnable)。而 Executors 是一个工具类,提供了一系列静态方法,帮助我们快速创建不同类型的线程池。”
“不过,在实际生产中,并不推荐直接使用 Executors 提供的工厂方法,因为它们背后隐藏了一些不易察觉的风险,例如 CachedThreadPool 和 FixedThreadPool 默认使用无界队列或无限线程,容易导致 OOM 或线程爆炸。”
“更推荐我们自己使用 ThreadPoolExecutor 构造函数,明确设置核心线程数、最大线程数、队列大小和拒绝策略,来精细控制线程池行为。”
这才是让面试官眼前一亮的回答。
小米的线程池小抄(收藏级)
推荐写法:
参数解释:
- 核心线程数:保留的线程数,长期存活。
- 最大线程数:并发高峰时,线程可以临时扩展到这个数量。
- 空闲线程保活时间:超过 core 的线程,在空闲多久后被回收。
- 队列:任务等待的地方,设置有界避免堆积。
- 拒绝策略:任务超载时怎么办?Abort(抛异常)、CallerRuns(当前线程执行)、Discard(丢弃)、DiscardOldest(丢最早的)。
结语:基础题里藏着底层逻辑
那次面试,我虽然在“Executor vs Executors”这一题没拿满分,但却在复盘中成长巨大。
很多时候,不是我们不会,而是没“透过源码看本质” 。
最后,我用一句话来总结本文,也送给你:
“Executors 让你写得快,ThreadPoolExecutor 让你跑得稳。”
别再让“无界队列”、“线程爆炸”、“OOM”等等这些坑在生产中炸锅。写代码,不只是能跑,而是能跑得久、跑得稳。
END
你还想了解哪些面试题? 欢迎留言,我们下次继续“面试复盘故事”系列!
如果你喜欢这样的讲解方式,别忘了 点赞 + 在看 + 分享,你的支持是我继续写作的最大动力!
我是小米,一个喜欢分享技术的31岁程序员。如果你喜欢我的文章,欢迎关注我的微信公众号“软件求生”,获取更多技术干货!