当前位置: 首页 > news >正文

线程池常见面试题

线程如何执行任务

execute() → addWorker() → worker.start() → run() 方法 → runWorker() 方法 → 取 firstTask 或者 通过 getTask() 获取队列中的任务 → task.run()

线程是如何从任务队列中取任务的?(线程是如何复用的?)

线程会保存在一个 HashSet<worker> 中 workers,worker 可以视为工作线程,(实际上是有一个线程成员变量,并且继承了 AQS,目的是简化获取和释放围绕每个任务执行的锁。这样可以防止那些唤醒等待任务的工作线程的中断,中断了正在运行的任务。)。线程通过 getTask() 方法,从 workQueue 中取任务。

当线程数大于核心线程数或者 allowCoreThreadTimeOut 设置为 true 时,会调用 workQueue 的 poll(keepAliveTime, TimeUnit.NANOSECONDS) 方法,如果任务队列中有任务,则返回并删除第一个任务,如果没有则等待 keepAliveTime 之后返回 null。

当线程数小于核心线程数并且 allowCoreThreadTimeOut 设置为 false 时,会调用 workQueue 的 take() 方法,如果任务队列中有任务,则返回并删除第一个任务,没有则一直等待。

非核心线程到达最大存活时间后是怎么销毁的?

当任务队列中没有任务时,经过 keepAliveTime 后 getTask() 会返回 null,进入到 processWorkerExit() 方法,这个方法会使 workers 删除 worker。

线程池内部的线程出现异常,上层调用线程池的地方能够感知到异常吗

当线程池中的线程执行任务时,如果该任务在运行过程中抛出异常,线程池本身不会直接传播异常到上层调用线程池的地方。因为线程池中的任务通常是通过实现 RunnableCallable 接口提交的,这些任务的执行发生在线程池的线程内部,而异常发生时,线程池会捕获并处理这些异常,而不是向外抛出。因此,上层调用线程池的代码通常不会直接感知到任务内部的异常。

具体分析:

  1. Runnable 异常处理
    当使用 Runnable 接口时,任务的 run() 方法不能抛出受检异常(checked exception),即使在 run() 方法中抛出未受检异常(unchecked exception),也不会被上层线程池感知。任务线程内部抛出的异常只会终止该线程的执行,但不会影响上层调用线程池的代码。

  2. Callable 异常处理
    Runnable 不同,Callable 接口的 call() 方法可以抛出异常。若任务抛出异常,上层代码通过 Future.get() 方法获取任务结果时,可以捕获到异常。但前提是调用了 Future.get(),否则任务的异常不会被显式捕获。

如何感知异常?

  • 通过 Future
    使用线程池执行任务时,如果使用 Callable 并通过 submit() 方法提交任务,线程池会返回一个 Future 对象。通过调用 Future.get(),上层代码可以感知任务内部是否有异常发生。如果 call() 方法抛出异常,Future.get() 会抛出一个 ExecutionException,可以通过该异常获取任务中的实际异常。

    ExecutorService executor = Executors.newFixedThreadPool(2);
    Callable<Integer> task = () -> {throw new RuntimeException("Task exception");
    };Future<Integer> future = executor.submit(task);
    try {future.get();  // 此处将抛出 ExecutionException
    } catch (ExecutionException e) {System.out.println("Caught exception: " + e.getCause());  // 打印出实际的异常原因
    }
    
  • 自定义线程工厂或异常处理机制
    可以通过自定义线程工厂,或使用 ThreadPoolExecutor 提供的 afterExecute() 方法来处理线程池中任务的异常。

    例如,可以覆盖 ThreadPoolExecutorafterExecute() 方法:

    ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 4, 0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<>());
    executor.execute(() -> {throw new RuntimeException("Task exception");
    });executor.setThreadFactory(runnable -> {Thread thread = new Thread(runnable);thread.setUncaughtExceptionHandler((t, e) -> System.out.println("Uncaught exception: " + e));return thread;
    });
    

通过 setUncaughtExceptionHandler,可以捕获到线程执行中的未受检异常。

总结:

  • 对于 Runnable 任务,上层无法直接感知异常,除非通过其他手段捕获。
  • 对于 Callable 任务,通过 Future.get() 可以感知到异常。
  • 如果需要全局处理异常,可以自定义线程池的异常处理机制,比如通过 ThreadFactory 设置线程的未捕获异常处理器,或者扩展 ThreadPoolExecutor 的方法。

线程池为什么到达核心线程数时会把任务放入任务队列,然后队列满了再增加线程到最大线程数,而不是直接到最大线程数?

  1. 资源效率的考虑。
    线程是系统的一种资源,创建和销毁线程是有开销的,过多的线程会导致资源浪费。Java 线程池在设计上遵循了“以尽可能少的线程处理尽可能多的任务”的原则。当线程数还未达到核心线程数时,优先创建新线程处理任务;达到核心线程数后,任务会进入队列等待,避免过度创建线程。只有当队列满了,任务无法被立即处理时,才会继续创建线程,直到达到最大线程数。这种机制可以更好地平衡性能和资源消耗。
  2. 提升线程复用率
    如果直接将线程数扩展到最大,那么会造成很多线程闲置,降低了线程的复用率。通过先使用核心线程,再使用任务队列来缓存任务,可以提高现有线程的利用率,避免不必要的线程切换和资源占用。
  3. 降低并发管理难度
    同时管理大量线程会增加 CPU 的调度压力,尤其是在处理 IO 密集型任务时,过多的线程可能导致频繁的上下文切换,反而会影响系统性能。通过先限制线程数量,逐步增加线程数量,可以有效降低系统的并发管理难度,保证任务处理的稳定性。
  4. 避免线程爆炸
    如果一开始就直接创建大量线程,可能会导致“线程爆炸”现象,消耗大量内存、CPU,进而拖垮整个系统。通过这种分阶段增加线程的设计,可以更好地控制线程增长的速度,防止过度并发导致系统崩溃。

可以总结为:Java 线程池通过先利用核心线程,再借助队列缓存任务,最后在必要时增加线程数,目的是为了节省系统资源、提高线程复用率、减小并发管理压力、以及避免线程爆炸。


http://www.mrgr.cn/news/56983.html

相关文章:

  • 编程新手小白入门最佳攻略
  • 代理 IP 在 AI 爬虫中的关键应用
  • SpringBoot集成Spring security 2024.10(Spring Security 6.3.3)
  • C++list
  • [单master节点k8s部署]41.部署springcloud项目
  • 《Python游戏编程入门》注-第2章2
  • hadoop
  • linux 编译安装的php7.4 开启pgsql,pdo_pgsql的扩展
  • 软件设计师考试大纲整理
  • JavaEE进阶----18.<Mybatis补充($和#的区别+数据库连接池)>
  • 如何设置Page Cache的大小为默认值
  • 32 类和对象 · 中
  • 卡牌抽卡机小程序,带来新鲜有趣的拆卡体验
  • 2025秋招八股文--mysql篇
  • 日志分析工具-应急响应实战笔记
  • 网络不稳定?试试这款Figma的中文替代设计工具
  • LLaMA Factory环境配置
  • ERP、SCM与CRM:三大系统的区别与整合策略
  • Go语言开发环境搭建
  • 源代码防泄密技术正在更新迭代中
  • curl请求接口的三个坑
  • 117.WEB渗透测试-信息收集-ARL(8)
  • STM32CubeMX软件界面不清晰调整方法
  • 专利交易:开启知识产权变现之门
  • 大厂面试真题-说说Clickhouse比Hbase强在哪
  • linux之网络子系统-路由子系统(1)