线程池常见面试题
线程如何执行任务
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。
线程池内部的线程出现异常,上层调用线程池的地方能够感知到异常吗
当线程池中的线程执行任务时,如果该任务在运行过程中抛出异常,线程池本身不会直接传播异常到上层调用线程池的地方。因为线程池中的任务通常是通过实现 Runnable
或 Callable
接口提交的,这些任务的执行发生在线程池的线程内部,而异常发生时,线程池会捕获并处理这些异常,而不是向外抛出。因此,上层调用线程池的代码通常不会直接感知到任务内部的异常。
具体分析:
-
Runnable
异常处理
当使用Runnable
接口时,任务的run()
方法不能抛出受检异常(checked exception),即使在run()
方法中抛出未受检异常(unchecked exception),也不会被上层线程池感知。任务线程内部抛出的异常只会终止该线程的执行,但不会影响上层调用线程池的代码。 -
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()
方法来处理线程池中任务的异常。例如,可以覆盖
ThreadPoolExecutor
的afterExecute()
方法: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
的方法。
线程池为什么到达核心线程数时会把任务放入任务队列,然后队列满了再增加线程到最大线程数,而不是直接到最大线程数?
- 资源效率的考虑。
线程是系统的一种资源,创建和销毁线程是有开销的,过多的线程会导致资源浪费。Java 线程池在设计上遵循了“以尽可能少的线程处理尽可能多的任务”的原则。当线程数还未达到核心线程数时,优先创建新线程处理任务;达到核心线程数后,任务会进入队列等待,避免过度创建线程。只有当队列满了,任务无法被立即处理时,才会继续创建线程,直到达到最大线程数。这种机制可以更好地平衡性能和资源消耗。 - 提升线程复用率
如果直接将线程数扩展到最大,那么会造成很多线程闲置,降低了线程的复用率。通过先使用核心线程,再使用任务队列来缓存任务,可以提高现有线程的利用率,避免不必要的线程切换和资源占用。 - 降低并发管理难度
同时管理大量线程会增加 CPU 的调度压力,尤其是在处理 IO 密集型任务时,过多的线程可能导致频繁的上下文切换,反而会影响系统性能。通过先限制线程数量,逐步增加线程数量,可以有效降低系统的并发管理难度,保证任务处理的稳定性。 - 避免线程爆炸
如果一开始就直接创建大量线程,可能会导致“线程爆炸”现象,消耗大量内存、CPU,进而拖垮整个系统。通过这种分阶段增加线程的设计,可以更好地控制线程增长的速度,防止过度并发导致系统崩溃。
可以总结为:Java 线程池通过先利用核心线程,再借助队列缓存任务,最后在必要时增加线程数,目的是为了节省系统资源、提高线程复用率、减小并发管理压力、以及避免线程爆炸。