• 面试官:Java线程池是怎么统计线程的空闲时间的?


    背景介绍:

    你刚从学校毕业后,到新公司实习,试用期又被毕业,然后你又不得不出来面试,好在面试的时候碰到个美女面试官!

    面试官: 小伙子,我看你简历上写的项目中用到了线程池,你知道线程池是怎样实现复用线程的?

    这面试官是不是想坑我?是不是摆明了不让我通过?

    难道你不应该问线程池有哪些核心参数?每个参数具体作用是什么?

    往线程池中不断提交任务,线程池的处理流程是什么?

    这些才是你应该问的,这些八股文我已经背熟了,你不问,瞎问什么复用线程?

    幸亏我看了一灯的八股文,听我给你背一遍!

    我: 线程池复用线程的逻辑很简单,就是在线程启动后,通过while死循环,不断从阻塞队列中拉取任务,从而达到了复用线程的目的。

    具体源码如下:

    1. // 线程执行入口
    2. public void run() {
    3. runWorker(this);
    4. }
    5. // 线程运行核心方法
    6. final void runWorker(Worker w) {
    7. Thread wt = Thread.currentThread();
    8. Runnable task = w.firstTask;
    9. w.firstTask = null;
    10. w.unlock();
    11. boolean completedAbruptly = true;
    12. try {
    13. // 1. 使用while死循环,不断从阻塞队列中拉取任务
    14. while (task != null || (task = getTask()) != null) {
    15. // 加锁,保证thread不被其他线程中断(除非线程池被中断)
    16. w.lock();
    17. // 2. 校验线程池状态,是否需要中断当前线程
    18. if ((runStateAtLeast(ctl.get(), STOP) ||
    19. (Thread.interrupted() &&
    20. runStateAtLeast(ctl.get(), STOP))) &&
    21. !wt.isInterrupted())
    22. wt.interrupt();
    23. try {
    24. beforeExecute(wt, task);
    25. Throwable thrown = null;
    26. try {
    27. // 3. 执行run方法
    28. task.run();
    29. } catch (RuntimeException x) {
    30. thrown = x;
    31. throw x;
    32. } catch (Error x) {
    33. thrown = x;
    34. throw x;
    35. } catch (Throwable x) {
    36. thrown = x;
    37. throw new Error(x);
    38. } finally {
    39. afterExecute(task, thrown);
    40. }
    41. } finally {
    42. task = null;
    43. w.completedTasks++;
    44. w.unlock();
    45. }
    46. }
    47. completedAbruptly = false;
    48. } finally {
    49. processWorkerExit(w, completedAbruptly);
    50. }
    51. }
    52. 复制代码

    runWorker方法逻辑很简单,就是不断从阻塞队列中拉取任务并执行。

    面试官: 小伙子,有点东西。我们都知道线程池会回收超过空闲时间的线程,那么线程池是怎么统计线程的空闲时间的?

    美女面试官的问题真刁钻,让人头疼啊!这问的也太深了吧!

    没看过源码的话,真不好回答。

    我: 嗯...,可能是有个监控线程在后台不停的统计每个线程的空闲时间,看到线程的空闲时间超过阈值的时候,就回收掉。

    面试官: 小伙子,你的想法挺不错,逻辑很严谨,你确定线程池内部是这么实现的吗?

    问得我有点不自信了,没看过源码不能瞎蒙。

    我还是去瞅一眼一灯写的八股文吧。

    我: 这个我知道,线程池统计线程的空闲时间的实现逻辑很简单。

    阻塞队列(BlockingQueue)提供了一个 poll(time, unit) 方法,作用就是:

    当队列为空时,会阻塞指定时间,然后返回null。

    线程池就是就是利用阻塞队列的这个方法,如果在指定时间内拉取不到任务,就表示该线程的存活时间已经超过阈值了,就要被回收了。

    具体源码如下:

    1. // 从阻塞队列中拉取任务
    2. private Runnable getTask() {
    3. boolean timedOut = false;
    4. for (; ; ) {
    5. int c = ctl.get();
    6. int rs = runStateOf(c);
    7. // 1. 如果线程池已经停了,或者阻塞队列是空,就回收当前线程
    8. if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
    9. decrementWorkerCount();
    10. return null;
    11. }
    12. int wc = workerCountOf(c);
    13. // 2. 再次判断是否需要回收线程
    14. boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
    15. if ((wc > maximumPoolSize || (timed && timedOut))
    16. && (wc > 1 || workQueue.isEmpty())) {
    17. if (compareAndDecrementWorkerCount(c))
    18. return null;
    19. continue;
    20. }
    21. try {
    22. // 3. 在指定时间内,从阻塞队列中拉取任务
    23. Runnable r = timed ?
    24. workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
    25. workQueue.take();
    26. if (r != null)
    27. return r;
    28. // 4. 如果没有拉取到任务,就标识该线程已超时
    29. timedOut = true;
    30. } catch (InterruptedException retry) {
    31. timedOut = false;
    32. }
    33. }
    34. }
    35. 复制代码

    面试官: 小伙子,可以啊,你是懂线程池源码的。再问你个问题,如果线程池抛异常了,也没有try/catch,会发生什么?

    美女面试官你这是准备打破砂锅问到底,铁了心不让我过,是吧?

    我的代码风格是很严谨的,谁写的业务代码不try/catch,也没遇到过这种情况。

    让我再看一下一灯总结的八股文吧。

    我: 有了,线程池中的代码如果抛异常了,也没有try/catch,会从线程池中删除这个异常线程,并创建一个新线程。

    不信的话,我们可以测试验证一下:

    1. /**
    2. * @author 一灯架构
    3. * @apiNote 线程池示例
    4. **/
    5. public class ThreadPoolDemo {
    6. public static void main(String[] args) {
    7. List<Integer> list = new ArrayList<>();
    8. // 1. 创建一个单个线程的线程池
    9. ExecutorService executorService = Executors.newSingleThreadExecutor();
    10. // 2. 往线程池中提交3个任务
    11. for (int i = 0; i < 3; i++) {
    12. executorService.execute(() -> {
    13. System.out.println(Thread.currentThread().getName() + " 关注公众号:一灯架构");
    14. throw new RuntimeException("抛异常了!");
    15. });
    16. }
    17. // 3. 关闭线程池
    18. executorService.shutdown();
    19. }
    20. }
    21. 复制代码

    输出结果:

    1. pool-1-thread-1 关注公众号:一灯架构
    2. pool-1-thread-2 关注公众号:一灯架构
    3. pool-1-thread-3 关注公众号:一灯架构
    4. Exception in thread "pool-1-thread-1" java.lang.RuntimeException: 抛异常了!
    5. at com.yideng.SynchronousQueueDemo.lambda$main$0(ThreadPoolDemo.java:21)
    6. at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
    7. at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
    8. at java.lang.Thread.run(Thread.java:748)
    9. Exception in thread "pool-1-thread-2" java.lang.RuntimeException: 抛异常了!
    10. at com.yideng.SynchronousQueueDemo.lambda$main$0(ThreadPoolDemo.java:21)
    11. at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
    12. at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
    13. at java.lang.Thread.run(Thread.java:748)
    14. Exception in thread "pool-1-thread-3" java.lang.RuntimeException: 抛异常了!
    15. at com.yideng.SynchronousQueueDemo.lambda$main$0(ThreadPoolDemo.java:21)
    16. at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
    17. at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
    18. at java.lang.Thread.run(Thread.java:748)
    19. 复制代码

    从输出结果中可以看出,线程名称并不是同一个,而是累加的,说明原线程已经被回收,新建了个线程。

    我们再看一下源码,验证一下:

    1. // 线程抛异常后,退出逻辑
    2. private void processWorkerExit(ThreadPoolExecutor.Worker w, boolean completedAbruptly) {
    3. if (completedAbruptly)
    4. decrementWorkerCount();
    5. final ReentrantLock mainLock = this.mainLock;
    6. mainLock.lock();
    7. try {
    8. completedTaskCount += w.completedTasks;
    9. // 1. 从工作线程中删除当前线程
    10. workers.remove(w);
    11. } finally {
    12. mainLock.unlock();
    13. }
    14. // 2. 中断当前线程
    15. tryTerminate();
    16. int c = ctl.get();
    17. if (runStateLessThan(c, STOP)) {
    18. if (!completedAbruptly) {
    19. int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
    20. if (min == 0 && !workQueue.isEmpty())
    21. min = 1;
    22. if (workerCountOf(c) >= min)
    23. return; // replacement not needed
    24. }
    25. // 3. 新建一个线程
    26. addWorker(null, false);
    27. }
    28. }
    29. 复制代码

    如果想统一处理异常,可以自定义线程创建工厂,在工厂里面设置异常处理逻辑。

    1. /**
    2. * @author 一灯架构
    3. * @apiNote 线程池示例
    4. **/
    5. public class ThreadPoolDemo {
    6. public static void main(String[] args) {
    7. List<Integer> list = new ArrayList<>();
    8. // 1. 创建一个单个线程的线程池
    9. ExecutorService executorService = Executors.newSingleThreadExecutor(runnable -> {
    10. // 2. 自定义线程创建工厂,并设置异常处理逻辑
    11. Thread thread = new Thread(runnable);
    12. thread.setUncaughtExceptionHandler((t, e) -> {
    13. System.out.println("捕获到异常:" + e.getMessage());
    14. });
    15. return thread;
    16. });
    17. // 3. 往线程池中提交3个任务
    18. for (int i = 0; i < 3; i++) {
    19. executorService.execute(() -> {
    20. System.out.println(Thread.currentThread().getName() + " 关注公众号:一灯架构");
    21. throw new RuntimeException("抛异常了!");
    22. });
    23. }
    24. // 4. 关闭线程池
    25. executorService.shutdown();
    26. }
    27. }
    28. 复制代码

    输出结果:

    1. Thread-0 :一灯架构
    2. 捕获到异常:抛异常了!
    3. Thread-1 :一灯架构
    4. 捕获到异常:抛异常了!
    5. Thread-2 :一灯架构
    6. 捕获到异常:抛异常了!
    7. 复制代码

    面试官: 小伙子,论源码,还是得看你,还是你背的熟。现在我就给你发offer,薪资直接涨10%,明天9点就来上班吧,咱们公司实行996工作制。

  • 相关阅读:
    CVE-2016-4977 Spring远程代码执行漏洞复现 POC、EXP在文末
    装饰器模式
    4种Javascript类型检测的方式
    【Mysql性能优化系列】MySQL优化WHERE语句18个硬核技巧
    「Redis数据结构」哈希表(Dict)
    kernel heap bypass smep,smap && 劫持modprobe_path
    day14-文件系统工作流程分析
    高等教育学:教学组织形式与教学工作基本环节
    分页列表缓存,你真的会吗
    基于蚁群算法的多配送中心的车辆调度问题的研究(Matlab代码实现)
  • 原文地址:https://blog.csdn.net/m0_73311735/article/details/127749507