🤔 面试官问:“你能说说在 Java 里创建多线程有哪几种方式吗?在实际的项目开发中,我们通常会用哪种方式来管理线程?”
📚 基础的三种方式(面试教科书答案)
- 继承
Thread类 最简单粗暴的方法。自己写一个类继承Thread,把要干的活写在run()方法里,然后start()启动。
- 缺点:Java 是单继承的。如果你继承了
Thread,就不能再继承其他类了,在项目里显得极其死板,所以极少使用。
实现
Runnable接口 最常用的基础方法。写一个类实现Runnable接口。因为是实现接口,你的类还可以自由继承别的类,扩展性很好。实现
Callable接口 前面两种方法都有一个致命弱点:干完活没有返回值(比如你想让线程帮你算个账,它算完没法直接把结果返回给你),而且不能抛出异常。如果你需要返回值,就必须实现Callable接口。
💼 实际项目中的终极方案:线程池(Thread Pool)
如果面试官问:“实际项目中怎么管理线程?” 千万不要说手动
new Thread()!
正确答案是:必须使用线程池(Thread Pool)统一管理!
为什么要用线程池?我们可以用一个”开公司”的通俗例子来理解:
手动
new Thread()的灾难:假设你开了一家外包公司,每接一个小项目,你就跑到大马路上临时招一个员工(new Thread()),等他把项目做完,你立刻把他开除(线程销毁)。 招人和走离职流程极其耗费时间和系统资源。更可怕的是,如果双十一突然来了 1 万个并发请求,系统瞬间去new一万个线程,服务器的内存会瞬间被挤爆(OOM),直接死机。使用线程池的优势:聪明的做法是,公司长期养着一个固定人数的”核心开发团队”(线程池里的核心线程)。 有了新项目,直接丢给团队里的闲人去做,做完不用开除,接着做下一个项目。不仅响应速度极快(省去了创建和销毁的时间),而且能控制最大并发数,保证服务器安全稳定运行。
🏆 面试完美话术总结:
“在 Java 中创建线程基础的有三种方式:继承
Thread类、实现Runnable接口和实现Callable接口。 但是在实际项目开发中,我们绝对不允许显式地手动去new Thread(),而是会统一使用线程池(ThreadPoolExecutor)来管理线程。因为手动创建线程会带来极大的创建和销毁开销,并且在面临高并发时无法控制线程数量,容易导致内存溢出。使用线程池可以复用已创建的线程,提高响应速度,并有效控制系统的最大并发量。”
🤔
面试官眼睛一亮问:“既然你提到了
ThreadPoolExecutor,你能说说它里面有哪几个极其重要的’核心参数’吗?假如任务太多了,线程池处理不过来,它会怎么办(拒绝策略)?”
为了把这个讲透,我们必须亮出 ThreadPoolExecutor 最核心的
4 个参数(面试必背):
corePoolSize(核心线程数):公司的”正式员工”编制。workQueue(工作队列/阻塞队列):公司的”大厅排队等候区”。maximumPoolSize(最大线程数):公司能容纳的”正式员工 + 临时工”的最大总人数。handler(拒绝策略):大厅挤爆了,临时工也招满了,对新来的客户宣读的”拒客令”。
当一个新任务(新客户)来到系统时,线程池是这样处理的(一定要记住这个顺序):
第一步(正式员工接客):如果当前工作的人数
< corePoolSize,直接马上创建一个新核心线程来干活。第二步(去大厅排队 - 极其关键):如果正式员工都在忙,此时绝对不会立刻扩容招人! 而是让这个新任务去
workQueue(队列)里排队待命。第三步(终于触发扩容了!):如果新任务源源不断,导致整个队列都塞满了!这个时候,为了防止系统崩溃,线程池终于开始”扩容”了。它会创建非核心线程(临时工)来火速处理新任务,直到总线程数达到
maximumPoolSize。第四步(大门紧闭,拒绝接客):如果队列满了,且总人数也达到了
maximumPoolSize(临时工也招不下了),这时候再来新任务,就会触发拒绝策略(handler)。
🚫 面试官必问:那拒绝策略有哪些呢?
Java 官方默认提供了 4 种拒绝策略,你只要能用大白话说出来就行:
AbortPolicy(默认):直接抛出RejectedExecutionException异常。就像保安直接把你轰出去,还大骂你一顿。CallerRunsPolicy:把任务退回给提交任务的那个线程去执行。就像老板把活儿甩给你,保安说:“我们不接,你自己去干吧!”(这个策略可以有效降低提交任务的速度,很常用)。DiscardPolicy:默默丢弃掉这个新任务,什么都不做,也不报错。就像保安当作没看见你。DiscardOldestPolicy:把队列里排在最前面(排得最久、最可怜)的那个任务踢掉,把新任务塞进去。就像保安把排在第一位的大冤种赶走,让你插队。
🤔 既然”临时工(非核心线程)“干完活了,公司总不能一直白养着他们吧?你知道线程池是怎么处理这些闲下来的临时工的吗?
当系统的高峰期过去,任务量降下来之后,“临时工”(非核心线程)在干完手头的活儿后,会去任务队列里拿下一个任务。如果队列已经空了(就像你说的没有饱满了,甚至没活儿了),它们就会进入”等待状态”。
如果在 keepAliveTime 设定的时间(比如 60
秒)内,它们依然没有等到新的任务,线程池就会认为”现在确实不需要这么多临时工了”,于是就会自动销毁这些非核心线程,把服务器的内存资源释放出来。
而那些”正式员工”(核心线程),默认情况下是永远不会被销毁的,他们会一直死等下一个任务(除非你特意开启了
allowCoreThreadTimeOut 参数)。