如果你是 JDK 设计者,如何设计线程池?我跟面试官大战了三十个回合(上)

简介: 如果你是 JDK 设计者,如何设计线程池?我跟面试官大战了三十个回合(上)

文章来源我自己公众号:如图两道面试题,顺便深入线程池,并连环17问

公众号很多硬核文章,求大家关注下呀~ 下面开始我们本篇文章。

image.png


今天就借着这两面试真题来深入一波线程池吧,这篇文章力求把线程池核心点和常问的面试点一网打尽,当然个人能力有限,可能会有遗漏,欢迎留言补充!

先把大部分问题列出来,如果你都答得出来,那没必要看下去:

  • 为什么会有线程池?
  • 简单手写一个线程池?
  • 为什么要把任务先放在任务队列里面,而不是把线程先拉满到最大线程数?
  • 线程池如何动态修改核心线程数和最大线程数?
  • 如果你是 JDK 设计者,如何设计?
  • 如果要让你设计一个线程池,你要怎么设计?
  • 你是如何理解核心线程的?
  • 你是怎么理解 KeepAliveTime 的?
  • 那 workQueue 有什么用?
  • 你是如何理解拒绝策略的?
  • 你说你看过源码,那你肯定知道线程池里的 ctl 是干嘛的咯?
  • 你知道线程池有几种状态吗?
  • 你知道线程池的状态是如何变迁的吗?
  • 如何修改原生线程池,使得可以先拉满线程数再入任务队列排队?
  • Tomcat 中的定制化线程池实现 如果线程池中的线程在执行任务的时候,抛异常了,会怎么样?
  • 原生线程池的核心线程一定伴随着任务慢慢创建的吗?
  • 线程池的核心线程在空闲的时候一定不会被回收吗?

接得住吗?话不多说,发车!


为什么会有线程池?


想要深入理解线程池的原理得先知道为什么需要线程池。

首先你要明白,线程是一个重资源,JVM 中的线程与操作系统的线程是一对一的关系,所以在 JVM 中每创建一个线程就需要调用操作系统提供的 API 创建线程,赋予资源,并且销毁线程同样也需要系统调用。

而系统调用就意味着上下文切换等开销,并且线程也是需要占用内存的,而内存也是珍贵的资源。

因此线程的创建和销毁是一个重操作,并且线程本身也占用资源。

然后你还需要知道,线程数并不是越多越好

我们都知道线程是 CPU 调度的最小单位,在单核时代,如果是纯运算的操作是不需要多线程的,一个线程一直执行运算即可。但如果这个线程正在等待 I/O 操作,此时 CPU 就处于空闲状态,这就浪费了 CPU 的算力,因此有了多线程,在某线程等待 I/O 等操作的时候,另一个线程顶上,充分利用 CPU,提高处理效率。


image.png


此时的多线程主要是为了提高 CPU 的利用率而提出。

而随着 CPU 的发展,核心数越来越多,能同时运行的线程数也提升了,此时的多线程不仅是为了提高单核 CPU 的利用率,也是为了充分利用多个核心。

至此想必应该明白了为什么会有多线程,无非就是为了充分利用 CPU 空闲的时间,一刻也不想让他停下来。

但 CPU 的核心数有限,同时能运行的线程数有限,所以需要根据调度算法切换执行的线程,而线程的切换需要开销,比如替换寄存器的内容、高速缓存的失效等等。

如果线程数太多,切换的频率就变高,可能使得多线程带来的好处抵不过线程切换带来的开销,得不偿失。

因此线程的数量需要得以控制,结合上述的描述可知,线程的数量与 CPU 核心数和 I/O 等待时长息息相关。

小结一下:

  • Java中线程与操作系统线程是一比一的关系。
  • 线程的创建和销毁是一个“较重”的操作。
  • 多线程的主要是为了提高 CPU 的利用率。
  • 线程的切换有开销,线程数的多少需要结合 CPU核心数与 I/O 等待占比。

综上我们知道了线程的这些特性,所以说它不是一个可以“随意拿捏”的东西,我们需要重视它,好好规划和管理它,充分利用硬件的能力,从而提升程序执行效率,所以线程池应运而生。


什么是线程池?


那我们要如何管理好线程呢?

因为线程数太少无法充分利用 CPU ,太多的话由于上下文切换的消耗又得不偿失,所以我们需要评估系统所要承载的并发量和所执行任务的特性,得出大致需要多少个线程数才能充分利用 CPU,因此需要控制线程数量

又因为线程的创建和销毁是一个“重”操作,所以我们需要避免线程频繁地创建与销毁,因此我们需要缓存一批线程,让它们时刻准备着执行任务。

目标已经很清晰了,弄一个池子,里面存放约定数量的线程,这就是线程池,一种池化技术。

熟悉对象池、连接池的朋友肯定对池化技术不陌生,一般池化技术的使用方式是从池子里拿出资源,然后使用,用完了之后归还。

但是线程池的实现不太一样,不是说我们从线程池里面拿一个线程来执行任务,等任务执行完了之后再归还线程,你可以想一下这样做是否合理。

线程池的常见实现更像是一个黑盒存在,我们设置好线程池的大小之后,直接往线程池里面丢任务,然后就不管了。


image.png


剥开来看,线程池其实是一个典型的生产者-消费者模式

线程池内部会有一个队列来存储我们提交的任务,而内部线程不断地从队列中索取任务来执行,这就是线程池最原始的执行机制。


image.png


按照这个思路,我们可以很容易的实现一个简单版线程池,想必看了下面这个代码实现,对线程池的核心原理就会了然于心。

首先线程池内需要定义两个成员变量,分别是阻塞队列和线程列表,然后自定义线程使它的任务就是不断的从阻塞队列中拿任务然后执行。

@Slf4j
public class YesThreadPool {
  BlockingQueue<Runnable> taskQueue;  //存放任务的阻塞队列
  List<YesThread> threads; //线程列表
  YesThreadPool(BlockingQueue<Runnable> taskQueue, int threadSize) {
    this.taskQueue = taskQueue;
    threads = new ArrayList<>(threadSize);
        // 初始化线程,并定义名称
    IntStream.rangeClosed(1, threadSize).forEach((i)-> {
      YesThread thread = new YesThread("yes-task-thread-" + i);
      thread.start();
      threads.add(thread);
    });
  }
    //提交任务只是往任务队列里面塞任务
  public void execute(Runnable task) throws InterruptedException {
    taskQueue.put(task);
  }
  class YesThread extends Thread { //自定义一个线程
    public YesThread(String name) {
      super(name);
    }
    @Override
    public void run() {
      while (true) { //死循环
        Runnable task = null;
        try {
          task = taskQueue.take(); //不断从任务队列获取任务
        } catch (InterruptedException e) {
          logger.error("记录点东西.....", e);
        }
        task.run(); //执行
      }
    }
  }
}

一个简单版线程池就完成了,简单吧!

再写个 main 方法用一用,丝滑,非常丝滑。

public static void main(String[] args) {
    YesThreadPool pool = new YesThreadPool(new LinkedBlockingQueue<>(10), 3);
    IntStream.rangeClosed(1, 5).forEach((i)-> {
      try {
        pool.execute(()-> {
          System.out.println(Thread.currentThread().getName() + " 公众号:yes的练级攻略");
        });
      } catch (InterruptedException e) {
        logger.error("记录点东西.....", e);
      }
    });
  }

运行结果如下:


image.png


下次面试官让你手写线程池,直接上这个简单版,然后他会开始让你优化,比如什么线程一开始都 start 了不好,想懒加载,然后xxxx...最终其实就是想往李老爷实现的 ThreadPoolExecutor 上面靠。

那就来嘛。


ThreadPoolExecutor 剖析


这玩意就是常被问的线程池的实现类了,先来看下构造函数:


image.png


核心原理其实和咱们上面实现的差不多,只是生产级别的那肯定是要考虑的更多,接下来我们就来看看此线程池的工作原理。

先来一张图:


image.png


简单来说线程池把任务的提交和任务的执行剥离开来,当一个任务被提交到线程池之后:

  • 如果此时线程数小于核心线程数,那么就会新起一个线程来执行当前的任务。
  • 如果此时线程数大于核心线程数,那么就会将任务塞入阻塞队列中,等待被执行。
  • 如果阻塞队列满了,并且此时线程数小于最大线程数,那么会创建新线程来执行当前任务。
  • 如果阻塞队列满了,并且此时线程数大于最大线程数,那么会采取拒绝策略。

以上就是任务提交给线程池后各种状况汇总,一个很容易出现理解错误的地方就是当线程数达到核心数的时候,任务是先入队,而不是先创建最大线程数。

从上述可知,线程池里的线程不是一开始就直接拉满的,是根据任务量开始慢慢增多的,这就算一种懒加载,到用的时候再创建线程,节省资源。





相关文章
|
2月前
|
监控 Kubernetes Java
阿里面试:5000qps访问一个500ms的接口,如何设计线程池的核心线程数、最大线程数? 需要多少台机器?
本文由40岁老架构师尼恩撰写,针对一线互联网企业的高频面试题“如何确定系统的最佳线程数”进行系统化梳理。文章详细介绍了线程池设计的三个核心步骤:理论预估、压测验证和监控调整,并结合实际案例(5000qps、500ms响应时间、4核8G机器)给出具体参数设置建议。此外,还提供了《尼恩Java面试宝典PDF》等资源,帮助读者提升技术能力,顺利通过大厂面试。关注【技术自由圈】公众号,回复“领电子书”获取更多学习资料。
|
1月前
|
数据采集 Java Linux
面试大神教你:如何巧妙回答线程优先级这个经典考题?
大家好,我是小米。本文通过故事讲解Java面试中常见的线程优先级问题。小明和小华的故事帮助理解线程优先级:高优先级线程更可能被调度执行,但并非越高越好。实际开发需权衡业务需求,合理设置优先级。掌握线程优先级不仅能写出高效代码,还能在面试中脱颖而出。最后,小张因深入分析成功拿下Offer。希望这篇文章能助你在面试中游刃有余!
45 4
面试大神教你:如何巧妙回答线程优先级这个经典考题?
|
27天前
|
Java 程序员 开发者
Java社招面试题:一个线程运行时发生异常会怎样?
大家好,我是小米。今天分享一个经典的 Java 面试题:线程运行时发生异常,程序会怎样处理?此问题考察 Java 线程和异常处理机制的理解。线程发生异常,默认会导致线程终止,但可以通过 try-catch 捕获并处理,避免影响其他线程。未捕获的异常可通过 Thread.UncaughtExceptionHandler 处理。线程池中的异常会被自动处理,不影响任务执行。希望这篇文章能帮助你深入理解 Java 线程异常处理机制,为面试做好准备。如果你觉得有帮助,欢迎收藏、转发!
105 14
|
1月前
|
缓存 安全 Java
面试中的难题:线程异步执行后如何共享数据?
本文通过一个面试故事,详细讲解了Java中线程内部开启异步操作后如何安全地共享数据。介绍了异步操作的基本概念及常见实现方式(如CompletableFuture、ExecutorService),并重点探讨了volatile关键字、CountDownLatch和CompletableFuture等工具在线程间数据共享中的应用,帮助读者理解线程安全和内存可见性问题。通过这些方法,可以有效解决多线程环境下的数据共享挑战,提升编程效率和代码健壮性。
67 6
|
2月前
|
算法 安全 Java
Java线程调度揭秘:从算法到策略,让你面试稳赢!
在社招面试中,关于线程调度和同步的相关问题常常让人感到棘手。今天,我们将深入解析Java中的线程调度算法、调度策略,探讨线程调度器、时间分片的工作原理,并带你了解常见的线程同步方法。让我们一起破解这些面试难题,提升你的Java并发编程技能!
101 16
|
2月前
|
安全 Java 程序员
面试直击:并发编程三要素+线程安全全攻略!
并发编程三要素为原子性、可见性和有序性,确保多线程操作的一致性和安全性。Java 中通过 `synchronized`、`Lock`、`volatile`、原子类和线程安全集合等机制保障线程安全。掌握这些概念和工具,能有效解决并发问题,编写高效稳定的多线程程序。
100 11
|
2月前
|
Java Linux 调度
硬核揭秘:线程与进程的底层原理,面试高分必备!
嘿,大家好!我是小米,29岁的技术爱好者。今天来聊聊线程和进程的区别。进程是操作系统中运行的程序实例,有独立内存空间;线程是进程内的最小执行单元,共享内存。创建进程开销大但更安全,线程轻量高效但易引发数据竞争。面试时可强调:进程是资源分配单位,线程是CPU调度单位。根据不同场景选择合适的并发模型,如高并发用线程池。希望这篇文章能帮你更好地理解并回答面试中的相关问题,祝你早日拿下心仪的offer!
52 6
|
2月前
|
缓存 安全 算法
Java 多线程 面试题
Java 多线程 相关基础面试题
|
3月前
|
缓存 安全 Java
【JavaEE】——单例模式引起的多线程安全问题:“饿汉/懒汉”模式,及解决思路和方法(面试高频)
单例模式下,“饿汉模式”,“懒汉模式”,单例模式下引起的线程安全问题,解锁思路和解决方法
|
3月前
|
Java 调度

相关实验场景

更多