程序员老鬼

京东面试官:线程池用过吗都有什么参数?底层如何实现的?

今天咱们聊聊Java面试中最常见的话题之一——线程池。相信大部分程序员都在面试中遇到过“你用过线程池吗?”这样的提问,对吧?

很多时候,这个问题并不只是想听你说你知道线程池的用法,而是更想知道你是否理解线程池的底层实现原理,以及它的各种参数配置。

线程池用过吗?我猜你肯定用过!

先说说线程池到底是啥。咱们做程序员的,应该都知道线程池是用来管理和复用线程的,避免频繁创建和销毁线程造成性能浪费。用线程池的好处就像用打车软件,明明有很多司机,你只需要预约一下,就能迅速得到服务,不用每次都等到有需求了才去找一个司机。而且司机不再是一个一个单独找,大家有一个统一的“管理平台”,就是线程池。

那么,在Java中,线程池的实现类是ExecutorService,它定义了线程池的基础功能,比如提交任务、关闭线程池等。而具体的线程池实现则是ThreadPoolExecutor,它控制了线程池的工作原理和行为。

线程池的几个核心参数

Java的线程池配置通常都通过ThreadPoolExecutor来实现。你可能在代码中见过类似这样的线程池初始化:

ExecutorService executor = new ThreadPoolExecutor(
        4, // corePoolSize
        10, // maximumPoolSize
        60L, // keepAliveTime
        TimeUnit.SECONDS, // time unit
        new LinkedBlockingQueue<Runnable>(100) // workQueue
);

这里面的参数其实挺多的,每一个都关系到线程池的性能和稳定性。下面我来一一解析:

  1. corePoolSize(核心线程数):这是线程池中始终保持活跃的线程数。即使线程池中的任务队列为空,核心线程仍然会保持活动状态。

    举个例子:假设你开了一家餐馆,核心线程就相当于你这家餐馆的厨师,始终在厨房忙碌,即使没有客人来,厨师也会一直在,随时准备好迎接客人的点单。

  2. maximumPoolSize(最大线程数):当线程池中的线程数达到核心线程数,但队列中任务仍然没有处理完时,线程池会继续创建新的线程,直到达到这个最大线程数。也就是说,当任务堆积得很严重时,线程池会增加临时线程来处理这些任务,但最大线程数也有限制。

    举个例子:假设你的餐馆原本有3个厨师,这就是核心线程数。当外面客人特别多,等位队伍排得像长蛇阵,餐馆决定临时增加厨师的数量,但最多只能招到10个。

  3. keepAliveTime(线程空闲时间):当线程池中的线程数超过了核心线程数,如果这些线程在keepAliveTime时间内没有任务执行,线程池就会回收这些空闲线程。也就是说,不再需要的线程会被销毁掉,避免资源浪费。

    举个例子:厨师闲着没事做,等了60秒还没接到新订单,那么这位厨师就可以下班休息,直到有新订单来。

  4. TimeUnit(时间单位):keepAliveTime的时间单位,常见的单位有SECONDS、MILLISECONDS等。

  5. workQueue(工作队列):任务提交到线程池时,会先进入到一个队列。队列的类型可以有很多种,常用的有:

  • LinkedBlockingQueue:这是一个基于链表的阻塞队列,队列大小可以指定(上面示例中的100),如果队列满了,线程池会创建新的线程,直到最大线程数为止。
  • ArrayBlockingQueue:一个基于数组的阻塞队列,队列的大小是固定的。
  • SynchronousQueue:每个插入操作必须等待一个删除操作,队列的容量是0,适合非常高并发的场景。
  • ThreadFactory(线程工厂):线程池用来创建新线程的工厂。如果不指定,默认使用Executors.defaultThreadFactory()来创建线程。可以自定义线程工厂,方便设置线程名称或设置线程的优先级。

  • RejectedExecutionHandler(拒绝策略):当线程池和队列都满了,任务提交者还会继续提交任务,线程池就需要采取一个策略来处理这些任务。常见的拒绝策略有:

    • AbortPolicy:直接抛出RejectedExecutionException异常(默认策略)。
    • CallerRunsPolicy:调用任务的run()方法,交给调用者线程来执行任务。
    • DiscardPolicy:直接丢弃任务,不抛出异常。
    • DiscardOldestPolicy:丢弃队列中最旧的任务,然后重新尝试提交任务。

    线程池的底层实现

    说到这里,相信大家对线程池的基本配置已经有了一个大概的了解。那么,线程池的底层是如何实现的呢?

    ThreadPoolExecutor底层是基于几个核心组件来管理线程的:

    • 工作队列(workQueue):这个队列用于缓存待执行的任务。队列满了后,ThreadPoolExecutor会根据配置决定是否创建新的线程。

    • 线程池(线程池中的线程):线程池中的线程由ThreadPoolExecutor管理,线程池会根据当前的线程池大小、队列状态等,动态调整线程的数量。

    • 线程工厂(ThreadFactory):负责创建线程对象,ThreadPoolExecutor会通过它来生成新线程。我们可以自定义线程工厂来实现一些特殊的需求,比如设置线程的名字、优先级等。

    • 拒绝策略(RejectedExecutionHandler):当队列已满且线程池中的线程都在忙碌时,ThreadPoolExecutor会根据拒绝策略来处理新提交的任务。默认情况下会抛出异常。

    一个典型的工作流程是这样的:

    1. 任务提交到线程池。
    2. 如果线程池中有空闲线程,则直接分配任务给线程池中的空闲线程。
    3. 如果线程池没有空闲线程并且队列没有满,那么任务就被加入到队列中。
    4. 如果线程池已经达到最大线程数,并且队列也满了,线程池会根据拒绝策略来处理任务。

    代码示例

    让我们看一段简单的代码,来感受一下线程池的使用:

    import java.util.concurrent.*;

    public class ThreadPoolDemo {
        public static void main(String[] args) {
            // 创建一个线程池,核心线程数为2,最大线程数为4,队列容量为10
            ExecutorService executor = new ThreadPoolExecutor(
                    2, 4, 60L, TimeUnit.SECONDS,
                    new LinkedBlockingQueue<>(10)
            );

            // 提交一些任务
            for (int i = 0; i < 15; i++) {
                int taskId = i;
                executor.submit(() -> {
                    System.out.println("Task " + taskId + " is running in thread: " + Thread.currentThread().getName());
                    try {
                        Thread.sleep(1000); // 模拟任务执行时间
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                });
            }

            // 关闭线程池
            executor.shutdown();
        }
    }

    这段代码展示了如何创建一个ThreadPoolExecutor并提交一些任务。你可以看到,线程池的大小是动态变化的,它会根据任务的数量和队列的状态自动调整线程数。

    如果你有更多问题,欢迎在评论区和我讨论。

    -END-

    ok,今天先说到这,老规矩,给大家分享一份不错的副业资料,感兴趣的同学找我领取。

    Image

    以上,就是今天的分享了,看完文章记得右下角给何老师点赞,也欢迎在评论区写下你的留言。