当前位置:   article > 正文

【面试题】线程池面试题及答案总结

【面试题】线程池面试题及答案总结

Java中创建线程的几种方式

在Java中创建线程主要有以下几种方式:

  • 继承Thread类:
    • 创建一个新的类,让它继承自java.lang.Thread。
    • 重写Thread类的run()方法,在这个方法中编写线程需要执行的任务代码。
    • 创建该子类的一个实例,并调用其start()方法启动线程。

示例代码

 public class MyThread extends Thread {
     @Override
     public void run() {
         System.out.println("通过继承Thread类的方式创建并运行线程");
     }

     public static void main(String[] args) {
         MyThread thread = new MyThread();
         thread.start();
     }
 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 实现Runnable接口

    • 创建一个类实现java.lang.Runnable接口。
    • 在该类中实现run()方法。
    • 使用Thread类作为包装器,将实现了Runnable接口的对象传递给Thread的构造函数,然后调用新建Thread对象的start()方法来启动线程。

    示例代码:

    public class RunnableTask implements Runnable {
           @Override
           public void run() {
               System.out.println("通过实现Runnable接口的方式创建并运行线程");
           }
           
           public static void main(String[] args) {
               RunnableTask task = new RunnableTask();
               Thread thread = new Thread(task);
               thread.start();
           }
       }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
  • 使用Callable和Future接口(结合ExecutorService):

    • 创建一个类实现java.util.concurrent.Callable接口,重写call()方法以定义线程要执行的任务,并返回一个结果。
    • 使用ExecutorService.submit(Callable)提交任务到线程池。
    • 通过获取返回的Future对象来处理线程完成后的结果或异常。

示例代码

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class CallableTask implements Callable<String> {
    @Override
    public String call() throws Exception {
        System.out.println("通过Callable和Future创建并运行线程");
        return "Task Result";
    }

    public static void main(String[] args) throws Exception {
        ExecutorService executor = Executors.newSingleThreadExecutor();
        CallableTask task = new CallableTask();
        Future<String> future = executor.submit(task);

        // 获取线程执行的结果
        String result = future.get();

        executor.shutdown(); // 关闭线程池
        System.out.println("线程执行结果:" + result);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 使用ExecutorService和ThreadPoolExecutor:
    • 虽然这并不是直接创建线程的方式,但是ExecutorService提供了管理和控制线程池的高级功能,包括线程的创建、调度和销毁等。
    • 可以通过Executors工具类提供的工厂方法创建不同类型的线程池,如固定大小的线程池、单个后台线程的线程池、可缓存线程的线程池等。

示例代码:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(5); // 创建固定大小的线程池

        for (int i = 0; i < 10; i++) {
            Runnable worker = () -> {
                System.out.println("线程:" + Thread.currentThread().getName() + "正在运行");
            };
            executor.execute(worker); // 提交任务到线程池
        }

        executor.shutdown(); // 关闭线程池,等待所有任务完成
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

以上是Java中创建和管理线程的主要方式,其中后两种方式更加强调了线程的复用和并发编程的高效性。


Java中创建线程池的方式以及它的构造方法的七大参数

在Java中,创建线程池的推荐方式是直接实例化java.util.concurrent.ThreadPoolExecutor类,而不是使用Executors工具类提供的静态工厂方法。尽管Executors提供了方便快捷的线程池创建方法,但这些预设配置可能不适合所有场景,尤其是在资源管理、异常处理和性能优化方面。

创建线程池的方式: 通过自定义ThreadPoolExecutor参数来创建线程池

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    corePoolSize,
    maximumPoolSize,
    keepAliveTime,
    unit,
    workQueue,
    threadFactory,
    handler
);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

七大构造方法参数详解:

  • corePoolSize:核心线程数,即线程池中的常驻线程数。即使这些线程空闲,也不会被终止,除非调用了allowCoreThreadTimeOut(true)方法或者线程池被关闭。
  • maximumPoolSize:线程池允许的最大线程数。当队列已满且有新任务提交时,如果当前线程数量小于最大线程数,则会创建新的线程来执行任务。
  • keepAliveTime:非核心线程闲置超时时长(单位由unit指定)。超过这个时间后,如果线程池中的线程数大于核心线程数,那么多余的线程将被终止。
  • unit:与keepAliveTime配套的时间单位,如TimeUnit.SECONDS、TimeUnit.MILLISECONDS等。
  • workQueue:工作队列,用于存储等待执行的任务。常见的队列实现有LinkedBlockingQueue(无界链表队列)ArrayBlockingQueue(有界数组队列)以及优先级队列PriorityBlockingQueue等。
  • threadFactory:线程工厂,用于创建新线程。默认的线程工厂创建的新线程都是非守护线程,并且具有相同的NORM_PRIORITY优先级。可以自定义线程工厂为线程设置名称、优先级或添加其他行为。
  • handler:拒绝策略,在线程池和工作队列都满了的情况下,无法接受新任务时所采用的策略。内置四种拒绝策略:
    • AbortPolicy:抛出RejectedExecutionException异常。
    • CallerRunsPolicy:调用者所在的线程自己执行该任务。
    • DiscardPolicy:默默丢弃任务,不抛出异常。
    • DiscardOldestPolicy:从工作队列中移除最早的任务(通常是最先阻塞的),然后尝试重新提交当前任务。

通过自定义上述参数,可以根据应用程序的实际需求来精细调整线程池的行为,以达到最佳的并发性能和资源利用效率。同时,也可以避免因预设线程池参数不当而引发的问题,比如内存溢出(OOM)或系统响应延迟等。


为什么不推荐使用ExecutorsService创建线程池

不推荐直接使用Executors类的静态工厂方法创建线程池主要有以下几个原因:

  • 缺乏灵活性和可定制性: Executors提供的几个预设线程池,如newFixedThreadPool, newSingleThreadExecutor, 和 newCachedThreadPool等,虽然易于使用,但它们往往具有固定的配置,并且在某些场景下可能不是最优选择。例如,预设的线程池通常采用无界队列(如LinkedBlockingQueue),这意味着如果任务提交速度远大于处理速度,可能导致内存泄漏。
  • 潜在的OOM风险:对于固定大小线程池(newFixedThreadPool)和单线程线程池(newSingleThreadExecutor),当工作队列设置为无界时,若大量任务堆积而无法及时消费,可能会耗尽系统内存。
    对于缓存线程池(newCachedThreadPool),如果不加以控制,它会根据需要无限增长线程数量,这也可能导致大量的线程消耗过多内存,最终引发OutOfMemoryError。
  • 默认线程工厂限制: 使用Executors创建的线程池,默认使用的是简单的线程工厂,生成的线程通常是非守护线程,没有自定义名称或优先级,这在一些对线程有特殊要求的应用场景中不够理想。
  • 资源管理和监控不便: 预设的线程池对于线程生命周期管理、异常处理、饱和策略等方面的配置较为有限,不利于进行精细化管理和问题排查。
  • 安全性和健壮性考虑: 在生产环境中,应确保线程池能够应对各种异常情况,例如合理地拒绝新任务、优雅地关闭线程池以及处理已提交任务的完成状态等。直接使用Executors创建的线程池可能不具备这些特性。

因此,在实际应用中,建议自定义ThreadPoolExecutor实例来创建线程池,这样可以更精确地控制线程池的大小、工作队列类型、饱和策略、线程工厂以及其他与线程池相关的参数,从而提高系统的稳定性和资源利用率。


介绍一下ForkJoinPool的基本原理和使用场景

ForkJoinPool是Java并发编程框架的一部分,它是Java 7引入的一个特殊的线程池实现,主要用于执行分治算法(Divide-and-Conquer)任务。其基本原理主要基于以下几个核心概念:

  • 工作窃取(Work Stealing): ForkJoinPool采用了工作窃取算法来提高并行处理的效率和CPU利用率。每个工作线程都有自己的双端队列来存放待处理的任务。当一个工作线程发现自己队列中的任务已经耗尽时,它会尝试从其他工作线程的队列尾部窃取任务来执行。这种机制确保了即使在非平衡任务分布情况下也能充分利用所有工作线程。
  • Fork/Join模型: 使用ForkJoinPool的任务通常继承自ForkJoinTask抽象类或者它的两个子类RecursiveAction和RecursiveTask。这些任务可以调用fork()方法将自己分割成更小的子任务,并递归地提交给线程池。当子任务完成后,通过调用join()方法合并结果。这样就可以将大任务分解为可独立计算的小任务,各个小任务并行计算后最终合并得到整个大任务的结果。
  • 任务调度与同步: ForkJoinPool内部维护了一套高效的任务调度系统,能够有效地管理和调度大量的并行任务。同时,ForkJoinTask提供了必要的同步原语,确保任务执行过程中的数据一致性以及正确地完成结果的合并。
  • 动态调整线程数量: ForkJoinPool默认根据处理器核心数创建相应数量的工作线程,并且可以根据运行时负载自动调整工作线程的数量,以适应系统资源的变化。

使用场景:

  • 大数据集的并行处理:如对大量数据进行排序、搜索等操作时,可以通过ForkJoinPool进行高效的并行计算。
  • 图像处理:在处理大规模图像运算或复杂图形渲染时,可以利用ForkJoinPool将复杂的像素运算拆分成多个子任务并行执行。
  • 科学计算和机器学习:在处理大型矩阵运算、数值计算、或者某些机器学习算法时,可以利用ForkJoinPool的优势进行快速并行计算。
  • 树形或图遍历算法:ForkJoinPool特别适用于那些具有天然分治性质的问题,例如深度优先搜索(DFS)、广度优先搜索(BFS)或其他形式的递归遍历。

总之,ForkJoinPool适合于处理那些可以被自然划分为一系列相互独立的子任务的并行计算场景,尤其是当任务规模动态变化并且需要高效利用多核处理器资源时。ForkJoinPool是一种特殊的线程池,特别适合处理能够进行递归分解的任务。它采用了工作窃取算法来提高并行效率,每个工作线程都有自己的双端队列,当一个工作线程的任务完成后,可以从其他线程的队列中窃取任务继续执行,从而充分利用CPU资源

在使用ForkJoinPool时,需要创建实现RecursiveActionRecursiveTask接口的任务类,并正确地对任务进行拆解和合并。同时,根据实际问题复杂度和硬件资源来调整线程池的大小


在使用线程池时,如何处理大量短生命周期的任务带来的性能瓶颈?

对于大量短生命周期的任务,可以考虑使用ThreadPoolExecutor配合一个无界或者有适当容量的队列(如SynchronousQueue或LinkedBlockingQueue),以减少线程创建和销毁开销。然而,需要注意的是,如果任务提交速度远大于处理速度,可能导致内存溢出。为防止这种情况,可以通过调整线程池大小、采用具有饱和策略的队列或其他自定义拒绝策略来控制任务积压。


如果发现线程池中的线程经常处于空闲状态,但系统响应仍然较慢,你会从哪些方面进行排查和优化?

  • 检查任务是否过于细粒度,导致线程大部分时间都在执行任务切换而非实际工作。
  • 分析是否存在大量的锁竞争或者其他同步机制引起的阻塞现象,导致线程无法有效执行任务。
  • 确认工作队列是否合适,如果是无界队列,可能存在任务积压但未被及时处理的情况。
  • 排查是否有外部资源限制(如数据库连接池满、网络带宽瓶颈)影响了线程的执行速度。
  • 对整个系统进行全面性能分析,包括CPU使用率、内存消耗、磁盘I/O等方面,找到潜在瓶颈并针对性地进行优化。

在高并发场景下,如何设计并选择合适的线程池大小?请详细解释你的决策过程。

  • 首先考虑系统的CPU核心数,通常将核心线程数设置为CPU核心数或其倍数可以充分利用硬件资源。
  • 考虑任务类型和执行时间,对于IO密集型任务,可以适当增大线程池大小,因为线程在等待IO操作时不会占用CPU;而对于CPU密集型任务,则应避免过度创建线程导致上下文切换频繁。
  • 分析系统负载情况和任务提交模式,通过压力测试确定最佳线程池大小,同时观察内存使用、响应时间和吞吐量等性能指标。
  • 使用可调节的线程池大小或者结合ThreadPoolExecutor的动态调整机制,在运行时根据负载自动调整线程数量。

当在线上环境发现线程池存在性能瓶颈时,你会如何定位和优化问题?

  • 定位问题:

    首先通过监控工具查看线程池的状态信息,包括当前活跃线程数、阻塞队列长度、已完成任务数量等指标。分析是否存在任务堆积、线程资源是否充分利用等问题。另外,查看系统日志查找是否有RejectedExecutionException异常抛出,这可能表明拒绝策略正在起作用。

  • 优化步骤:

    • 如果是任务积压严重,检查任务提交速率和处理速率之间的平衡,调整工作队列大小或者增加最大线程数。
    • 若发现线程资源浪费,如大量空闲线程,可能是核心线程数或线程存活时间设置不当,可根据实际情况进行调整。
    • 检查是否存在线程饥饿或死锁情况,确保任务分配公平性,必要时使用公平队列或定制调度策略。
    • 对于长耗时的任务,考虑采用异步回调机制或者拆分成更小的任务单元。
    • 分析业务逻辑,看是否有不必要的同步等待或资源竞争,优化代码逻辑以减少锁争抢。

在多级缓存或者服务间调用的异步处理场景中,线程池如何设计才能确保高效且稳定的性能表现?

  • 在这种场景下,线程池设计应关注隔离性和优先级控制,可能需要多个不同类型的线程池来分别处理不同的任务层级。
  • 对于I/O操作,可能会使用一个专门的线程池进行异步处理,避免阻塞主线程或者计算密集型任务的执行。
  • 可能需要引入优先级队列或者定制化的调度策略,确保高优先级的任务能够快速得到响应。
  • 根据服务间的依赖关系和性能需求,合理配置线程池容量和饱和策略,确保服务之间的负载均衡和稳定性。

在大型分布式系统中,多个模块都使用了线程池,如何避免全局资源竞争和死锁问题?

  • 统一管理和控制线程池的创建与销毁,确保整体资源不会过度分配。
  • 确保各个模块使用的线程池参数设定合理,避免单个模块的线程资源占用过高导致其他模块无法正常工作。
  • 使用有界队列避免无限制的任务堆积造成内存溢出,同时配合合理的拒绝策略。
  • 在任务之间尽量减少共享资源的访问,对于必须共享的资源,使用恰当的锁机制(如读写锁)降低锁竞争。
  • 遵循锁的获取顺序,预防死锁的发生,同时监控线程状态以便及时发现问题。

在设计高并发服务时,如何根据系统负载和业务需求合理地选择和配置线程池?请详细描述你的决策过程。

  • 分析系统的CPU核心数、内存资源以及I/O密集程度。
  • 根据任务特性确定是CPU密集型还是IO密集型,进而决定线程池的核心线程数是否需要接近或等于处理器核心数。
  • 选择合适的阻塞队列类型和大小以适应任务提交速率和处理速度之间的关系。
  • 设定合理的最大线程数以防止过度消耗系统资源,并设置适当的存活时间来回收空闲线程。
  • 考虑使用RejectedExecutionHandler拒绝策略应对饱和场景,如记录日志、丢弃任务或者直接执行等。
  • 对于周期性任务或延时任务,考虑使用ScheduledThreadPoolExecutor。
  • 结合业务特点,如任务优先级、依赖关系等因素调整线程池实现方案。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/很楠不爱3/article/detail/228679
推荐阅读
相关标签
  

闽ICP备14008679号