面试题:线程池

面试题:线程池

线程池这东西,说简单也简单,说复杂也能绕进死胡同。我刚入职那会儿,听到“线程池”三个字,脑子里就浮现出一群线程泡在热水里泡澡,谁来了就蹦出来干活。现在回头看,当时那个画面属实抽象了点,但居然也挺贴切——线程池的核心思想,还真就是“热水永远烧着,有活干就上,干完继续泡着等下一波”。

今天咱们就聊聊线程池到底是个什么玩意儿,它是怎么运作的,为什么说它是 Java 并发编程里的“中流砥柱”。放心,我不整那些死板的术语,全程口语化输出,看完你保准能明白这货怎么回事,甚至能在代码里整出点花活。

一、线程池到底是个啥?

我们先来点通俗的。

打个比方,线程就是工人,任务就是活儿,那你现在是老板,你有一堆活要安排人干。

你要是每来一单活,就临时去街上招个工人回来,等他干完再遣散,这时间成本高得吓人;如果活不稳定,工人你请也不是,不请也不是,工资白给了。

线程池这时候就像一个工人中介,你提前准备好了一批“熟练工”,把他们都安置在一个“候工区”里(也就是线程池),有活就安排他们上,干完回座继续等着。来活频繁的时候还能叫点临时工,但你自己得设个上限,防止人太多挤爆现场(OOM)。

这就是线程池的本质:提前准备好线程资源,复用线程,控制线程数量,避免频繁创建销毁线程带来的开销和不确定性。

二、为啥大家都在用线程池?

如果你还在写 new Thread(() -> doSomething()).start() 这种代码,得,我告诉你个现实:你这写法不仅不优雅,还很“原始”。在高并发场景下直接 new 线程,简直是 CPU 杀手,内存爆破器。

线程的创建、上下文切换、销毁这些操作都不是白来的,尤其是高并发下,一不留神就会造成系统性能雪崩。线程池就是来解决这个问题的。

线程池的好处主要有这几个:

  1. 省资源:线程创建和销毁本身就是系统开销,复用线程自然更节能环保;

  2. 响应快:活一来,线程现成的,马上上手;

  3. 可控性强:线程数量你说了算,活太多还可以设置队列或者拒绝策略,避免被“榨干”。

说白了,线程池就是你手下那批能打硬仗、讲效率、可管理的兵。

三、线程池怎么玩?

ThreadPoolExecutor 是 Java 线程池的灵魂人物,如果说 Executors 是个糖衣炮弹,那 ThreadPoolExecutor 就是你亲手定制的钢铁侠,配置自由、逻辑清晰。

它的构造方法长得像这样:

public ThreadPoolExecutor(
  int corePoolSize,
  int maximumPoolSize,
  long keepAliveTime,
  TimeUnit unit,
  BlockingQueue<Runnable> workQueue,
  ThreadFactory threadFactory,
  RejectedExecutionHandler handler
)

每个参数都不是摆设,我们一个个扒开讲。

核心线程数(corePoolSize)

这部分线程是线程池的“固定班底”,也就是工地上的常驻工人。只要有活,他们就上,没活他们也在等,除非你手动让他们“闲了就走”(用 allowCoreThreadTimeOut(true) 设置)。

最大线程数(maximumPoolSize)

这是线程池能招的最多的“工人总数”。一旦核心线程都忙不过来了,而且任务队列也塞满了,那只能临时招人(非核心线程)来帮忙,但你得设个上线,不然工地炸锅。

存活时间(keepAliveTime + unit)

非核心线程的“饭点时间”。如果他们干完活没事做,等的时间超过这个值就自动撤了,回老家种地去了。

队列(workQueue)

这个队列就是活的等待区。核心线程忙不过来了,任务就先排队;队列也满了?那就看能不能临时多招人;实在不行?那就用拒绝策略处理。

线程工厂(threadFactory)

就是你招聘用的“招人平台”,默认就行,一般不动它,除非你有特殊需求,比如给每个线程设置个名字啥的。

拒绝策略(handler)

真滴没线程用了,任务也排不进队列了,那咋整?这时候就得看你的“拒绝策略”了。

  • AbortPolicy(默认):直接抛异常,任务别想跑;

  • CallerRunsPolicy:你主线程干;

  • DiscardPolicy:悄无声息丢掉;

  • DiscardOldestPolicy:把队列里最早的任务扔掉,给新任务让路。

四、不要用 Executors 创建线程池

你可能见过这些写法:

Executors.newFixedThreadPool(10);
Executors.newSingleThreadExecutor();
Executors.newCachedThreadPool();

写起来确实爽,一行搞定。但是!《阿里巴巴 Java 开发手册》都告诉你别这么玩。

为啥?因为你根本不知道背后线程池的参数长啥样,尤其是队列容量和最大线程数这两个关键项,全都给你默认了:

比如说 newFixedThreadPool() 背后的代码其实是:

new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS,
                      new LinkedBlockingQueue<Runnable>());

LinkedBlockingQueue 默认大小是多少?Integer.MAX_VALUE

你以为你只创建了 10 个线程,其实你可能把任务排到队列里去了几百万个,JVM 笑嘻嘻地飘走,剩你拿着 OOM 的日志发呆。

所以,正确的做法是:用 ThreadPoolExecutor 自己配参数,明确每一个线程池的行为逻辑,不给 JVM 留“自由发挥”的空间。

五、核心线程会被回收吗?

默认不会。核心线程的设计目的就是“长期蹲守”,但你可以通过:

executor.allowCoreThreadTimeOut(true);

来让它们“闲了就撤”。前提是你得先设置 keepAliveTime 大于 0。

这点特别适用于那种“周期性任务高峰”的场景,比如说每天下午三点有一波流量,那你可以让核心线程在平时也别白吃饭,等够时间就回收。

六、线程是怎么从队列里取活干的?

这个细节很有意思。

线程池内部维护了一个 Worker 类,它其实就是包了一层的线程,不停地从队列里拿活:

  • 如果你没设置 allowCoreThreadTimeOut,或者这个线程是核心线程,它会用 take() —— 就是一直接收,干等也等;

  • 如果是非核心线程,那就用 poll() —— 超时没任务就走人。

核心线程的“死等”和非核心线程的“限时等”机制,其实也保证了线程池的弹性。

源码大概长这样:

Runnable r = timed ?
    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
    workQueue.take();

这就是线程池自我调节的地方。根据你的配置自动判断用多少人,保留多少人,走得也不含糊。

七、实战建议

写线程池时,强烈建议你自己手动配置,比如这样:

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    4,                   // 核心线程数
    8,                   // 最大线程数
    60,                  // 非核心线程最大空闲时间
    TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(100),    // 有界队列
    Executors.defaultThreadFactory(),
    new ThreadPoolExecutor.AbortPolicy()  // 拒绝策略
);

这个配置的含义是:我先招 4 个工人,有活他们先上;活太多了,最多扩到 8 个;任务先放队列,最多排 100 个;再多?我直接抛异常,你自己看着办。

这样配出来的线程池行为全在你掌控之中,不会突然因为队列太大或线程太多而崩掉。

写在最后

线程池这东西,看着像是 Java 高并发编程里的一个“基础组件”,但它的作用远比你想象的大。很多人写代码只关心业务逻辑,线程池就随手一写,结果一上线,内存飙了、CPU 爆了,然后一脸懵逼地看着报警信息说:“不是用了线程池吗?”

用线程池不是目的,用好线程池才是正道。

搞清楚线程池是怎么运作的,能省下你无数个线上救火的夜晚。

线程池Java中用于管理和复用线程的机制,它可以提高线程的利用率和性能。线程池的七大参数包括: 1. corePoolSize(核心线程数):线程池中始终保持的线程数量,即使它们处于空闲状态。当任务数量超过核心线程数时,线程池会创建新的线程来处理任务。 2. maximumPoolSize(最大线程数):线程池中允许存在的最大线程数量。当任务数量超过最大线程数时,新的任务会被放入等待队列中等待执行。 3. keepAliveTime(线程空闲时间):当线程池中的线程数量超过核心线程数时,多余的空闲线程在等待新任务到来时的最长等待时间。超过这个时间,空闲线程会被销毁。 4. unit(时间单位):用于设置keepAliveTime的时间单位,可以是秒、毫秒、分钟等。 5. workQueue(任务队列):用于存放等待执行的任务的队列。常见的队列类型有有界队列(如ArrayBlockingQueue)和无界队列(如LinkedBlockingQueue)。 6. threadFactory(线程工厂):用于创建新线程的工厂类。可以自定义线程的名称、优先级等属性。 7. handler(拒绝策略):当任务无法被线程池执行时的处理策略。常见的策略有直接抛出异常、丢弃任务、丢弃队列中最旧的任务等。 线程池的作用主要有以下几点: 1. 提高性能:线程池可以复用线程,避免了频繁创建和销毁线程的开销,提高了系统的性能。 2. 控制资源:通过设置核心线程数和最大线程数,可以控制系统中并发线程的数量,避免资源被过度占用。 3. 提供任务队列:线程池可以提供一个任务队列,用于存放等待执行的任务。当线程池中的线程都在执行任务时,新的任务会被放入队列中等待执行。 4. 管理线程:线程池可以统一管理线程的生命周期,包括创建、销毁、空闲时间等。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值