高效并发处理之必备利器:线程池

简介: 高效并发处理之必备利器:线程池


🌟 Java中的多线程编程

Java中的多线程编程是一种高效的程序设计方式,可以同时处理多个任务,从而提高程序的处理能力。但是线程的创建、调度和销毁都会消耗一定的系统资源,因此,在设计多线程程序时需要使用线程池来管理线程的生命周期。

Java提供了一个java.util.concurrent包,里面提供了一系列的线程池的实现类,开发人员可以通过这些类方便地创建、管理线程池。

在这些类中,最常用的就是Executors类,这个类可以方便地创建几种常见的线程池。

  1. newSingleThreadExecutos

这个方法创建的线程池只有一个线程,也就是说,线程池中只有一个任务在处理,这个方法比较适合需要保证任务顺序执行的场景。

如果需要同时处理多个任务,则不建议使用这个线程池。因为在这个线程池中,一旦当前任务出现异常或者阻塞,整个线程池就会停止工作,后续任务也无法执行。

  1. newFixedThreadPool(int nThreads)

这个方法创建的线程池是固定大小的线程池,线程池中同时运行的线程数量是固定的,即使有空闲线程也不会多开。

这个线程池比较适合处理占用资源较多的任务,比如网络IO操作、文件IO操作等。在这些操作中,线程往往会出现阻塞,因此需要线程池来管理线程的生命周期,并且限制线程的数量。

  1. newCachedThreadPool()

这个方法创建的线程池是无界线程池,也就是说,线程池中可以同时处理任意数量的任务,因为每个任务都会创建一个新的线程来处理。

这个线程池比较适合处理短时间、CPU轻度占用的任务,比如数据计算、图像处理等。如果任务较多,这个线程池可能会导致系统资源耗尽,因此需要合理的设置队列容量或者使用其他种类的线程池。

以下是Java中使用Executors类创建线程池的示例代码:

  1. 创建一个单线程的线程池
ExecutorService executor = Executors.newSingleThreadExecutor();
  1. 创建一个固定大小的线程池
ExecutorService executor = Executors.newFixedThreadPool(5);
  1. 创建一个可缓存的线程池
ExecutorService executor = Executors.newCachedThreadPool();

需要注意的是,在实际使用线程池的时候,不建议使用JDK提供的三种常见的创建方式。因为:

  1. Executors提供的线程池使用场景很有限,一般场景很难用到。
  2. 这些线程池都是通过ThreadPoolExecutor创建的,通过直接使用ThreadPoolExecutor创建线程池,可以理解原理,灵活度更高。
  3. Executors提供的线程池使用Linked队列,这个接近于无界,非常大,这样会堆积大量的请求,从而导致OOM,因此阿里巴巴开发手册推荐使用ThreadPoolExecutor创建线程池。

因此,在实际使用线程池的时候,需要根据实际情况选择合适的线程池类型,并使用ThreadPoolExecutor类进行创建和管理。同时,需要合理设置线程池参数,避免因线程池造成的性能问题。

🍊 数据结构

线程池的内部实现是通过一个线程池类 ThreadPoolExecutor 来完成的。ThreadPoolExecutor 具有下列重要的特征:

  • corePoolSize:核心线程数大小
  • maximumPoolSize:最大线程数
  • workQueue:线程池的任务队列
  • keepAliveTime:空闲线程等待新任务的超时时间
  • unit:keepAliveTime 的时间单位
  • threadFactory:用于创建新线程的工厂
  • handler:当添加任务失败时的策略

线程池中核心的数据结构是任务队列,用于存储已经提交的但尚未被执行的任务,亦可被视为缓冲。在 Java 中,线程池的任务队列支持多种类型的实现:

  • ArrayBlockingQueue:基于数组的有界队列,先进先出原则,当队列满了,就需要等待,直到有空位置。
  • LinkedBlockingQueue:基于链表的无界队列,先进先出原则,如果任务数大于核心线程数时,任务就会被放入任务队列,等待核心线程执行完毕后再执行。
  • PriorityBlockingQueue:基于优先级的无界队列,通过任务的优先级来进行排序。
  • SynchronousQueue:一个不存储元素的阻塞队列,每个插入操作必须等待另一个线程的移除操作,反之亦然。

线程池在执行任务时,通过选择合适的数据结构实现了多线程的调度、线程的复用,从而提高了应用程序的执行效率和线程使用效率。

🍊 线程池拒绝策略

线程池的拒绝策略是指在线程池的工作队列已满并且线程池中的线程达到最大数时,如何处理新提交的任务。不同的拒绝策略可以提高系统的稳定性和可靠性。

以下是线程池中的四种拒绝策略:

🎉 1. AbortPolicy拒绝策略

AbortPolicy是线程池默认的拒绝策略,也是最简单的拒绝策略。当线程池的工作队列已满并且线程池中的线程达到最大数时,此时再次提交任务将会直接抛出RejectedExecutionException异常,从而阻止系统正常运行。这种拒绝策略适用于临时任务提交的情况,例如在一个定时任务中,每次处理完任务后,需要再次提交一个新的任务。当达到最大限制时,这些任务可以被忽略,因为它们并不是必要的,只是一些额外的工作。

🎉 2. CallerRunsPolicy拒绝策略

CallerRunsPolicy是一种比较友好的拒绝策略。当线程池的工作队列已满并且线程池中的线程达到最大数时,此时再次提交任务将会将某些任务回退到调用者,让提交任务的线程来执行。也就是说,当线程池无法处理当前任务时,任务的执行权会交给提交任务的线程来执行。这种策略可以有效地降低新任务的流量,并保留提交任务的线程的执行权。如果任务比较耗时,提交任务的线程也会处于忙碌状态,无法继续提交任务。因此,这种策略可以减缓任务的提交速度,使得线程池中的线程有足够的时间来消化任务。从长远来看,这也会提高系统的稳定性和可靠性。

🎉 3. DiscardOldestPolicy拒绝策略

DiscardOldestPolicy是一种直接抛弃队列中等待时间最长的任务的拒绝策略。当线程池的工作队列已满并且线程池中的线程达到最大数时,此时再次提交任务将会抛弃队列中等待时间最长的任务,然后将当前任务加入队列尝试再次提交当前任务。这种策略可以有效地防止队列中的任务过多,从而保证新任务的正常提交。但是,抛弃最老的任务可能会有一定的风险,因为这些任务可能非常重要,并且被抛弃可能会对系统的稳定性和可靠性造成影响。

🎉 4. DiscardPolicy拒绝策略

DiscardPolicy是一种直接丢弃任务的拒绝策略,不会对提交任务进行任何处理或者抛出异常。当任务被提交时,直接将刚提交的任务丢弃,而且不会给与任何提示通知。这种策略虽然简单,但是对于一些重要的任务而言,可能会产生非常严重的后果。因此,如果任务的重要性比较高,不建议使用此策略。

综上所述,针对不同的业务需求,我们可以选择不同的线程池拒绝策略来保证系统的稳定性和可靠性。一般情况下,我们可以采用CallerRunsPolicy策略来保证任务不被丢弃,并将这些任务交给提交任务的线程来执行。如果任务量过大,可能需要采用DiscardOldestPolicy策略来丢弃等待时间最长的任务,从而保证新任务的正常提交。如果任务的重要性较低,我们可以使用DiscardPolicy策略来直接丢弃任务,但在处理重要任务时,不建议使用此策略。最后,需要提醒的是,在使用线程池时,一定要根据具体情况进行配置,以保证系统的稳定性和可靠性。

🍊 实战使用场景

线程池在大多数并发场景中都得到了广泛的应用,比如服务器接收大量的请求、webservice 接口,对于耗时的后台比如支付,发送短信等等都可以使用线程池来异步处理。在异步处理的同时,还可以提高响应速度和性能,防止服务器超负荷而崩溃。

另外,在一些需要随时处理任务的系统中,比如数据库连接池、消息队列、定时任务操作等,一般都会使用线程池,以达到高效稳定的目的。

🍊 遇到的问题和解决方案

  1. 线程池设置不当导致OOM问题

当线程池中的任务数量过多时,线程池可能会引发内存溢出(OOM)问题。此时,可以调整线程池的参数来解决该问题,例如增加空闲线程的存活时间、调整任务队列的大小等。

  1. 线程池拒绝策略导致任务执行失败

当线程池中的任务数量达到最大值时,线程池会按照预设的拒绝策略来处理新提交的任务,这有可能会导致一些任务无法被执行,从而影响应用程序的正常运行。为避免该问题,可以考虑调整拒绝策略,例如使用 DiscardOldestPolicy 策略丢弃任务队列中等待最久的任务。

  1. 线程池的大小设置不合理导致系统性能下降

线程池的大小设置不当会导致系统性能下降。如果线程池过小,那么任务就需要排队等待,从而影响系统的响应时间;而如果线程池过大,那么就会导致线程的上下文切换频繁,从而浪费系统资源。为避免该问题,可以根据系统的特点和负载情况,调整线程池的大小和核心线程数。

  1. 没有及时关闭线程池导致系统性能下降

当系统不再需要使用线程池时,如果没有及时关闭线程池,那么就会占用系统资源,从而导致系统性能下降。为避免该问题,应该在不需要使用线程池时,及时关闭线程池,释放系统资源。

  1. 线程池中出现死锁问题

当线程池中的任务存在相互依赖的关系时,可能会发生死锁问题。此时,可以通过合理的设计任务之间的依赖关系,或者通过使用并发控制工具类来解决该问题。

  1. 线程池中的任务出现异常问题

当线程池中的任务出现异常问题时,应该及时处理异常,避免线程池中的其他任务受到影响。可以通过设置 Thread.UncaughtExceptionHandler 或者使用 try-catch 块来捕获异常并处理。

以下是代码示例,展示如何处理线程池中的异常问题:

// 创建线程池
ExecutorService executor = Executors.newFixedThreadPool(5);
// 提交任务
executor.submit(new Runnable() {
    @Override
    public void run() {
        // 任务逻辑代码
        try {
            // 执行任务代码
        } catch (Exception e) {
            // 捕获异常并处理
            // 输出异常信息
            System.out.println(e.getMessage());
            // 定义异常处理逻辑
            // ...
        }
    }
});
// 关闭线程池
executor.shutdown();

以上代码示例中,我们通过 ExecutorService 来创建线程池,并通过 submit() 方法来提交任务。在任务中,使用 try-catch 块来捕获异常,然后在 catch 块中进行异常处理。最后,使用 shutdown() 方法来关闭线程池。这样可以保证线程池中的任务在出现异常时能够被及时处理,从而避免影响其他任务的执行。

🍊 如何合理的配置核心线程数?

在实际开发中,我们经常会遇到需要执行大量任务的场景,例如同时处理多个请求、并发下载等。为了提高任务执行效率,极大降低线程创建和销毁的开销,我们通常会使用线程池来管理线程。但是,线程池的设置并不是一成不变的,需根据具体情况进行选择和优化。

对于CPU密集型任务,因为这类任务通常需要大量的计算,而计算过程中需要 CPU 不停地工作,不需要进行 I/O 操作,因此这类任务需要较少的线程数,否则容易引起上下文切换的过度消耗。因此,推荐将线程池的核心线程数设置为 CPU 核心数加 1。这样能够充分利用 CPU 的性能,提高任务执行效率,减少线程创建和销毁的开销。同时,也可以最大程度地减小上下文切换的次数,保证 CPU 的高效运行。

对于I/O密集型任务,因为这类任务需要频繁进行 I/O 操作,而这些操作通常需要等待一定的时间才能完成,因此线程的 CPU 使用率并不高。在这种情况下,可以通过增加线程数来提高并发度,让 CPU 在等待 I/O 操作的时候去处理别的任务,充分利用 CPU。根据经验,通常建议将线程池的核心线程数设置为 2 * CPU 核心数。

对于混合型任务,需要考虑任务的具体情况。如果任务包含了大量的 I/O 操作,那么可以根据 I/O 密集型任务的设置参数进行设置;如果任务主要是 CPU 密集型,那么可以根据 CPU 密集型任务的设置参数进行设置。

最后,需要指出的是,线程池的设置并不是万能的。对于并发高、业务执行时间长的任务,可以从整体架构的设计入手,看看业务中某些数据是否能做缓存,是否有必要增加服务器等措施。如果任务执行时间过长,还可以考虑使用中间件对任务进行拆分和解耦,以提高效率和稳定性。总之,在实际开发中,需要根据具体情况进行选择和优化,才能充分发挥线程池的优势。

🍊 底层运行原理

线程池是一种重要的多线程编程技术,它可以在处理任务时有效地管理线程数量,提高系统的运行效率和性能。线程池的核心思想是通过维护一个线程队列和一个任务队列,来有效地分配和调度线程资源,实现高效的并发处理。

线程池的设计思想可以类比为银行网点,其中常驻核心数相当于银行的今日当值窗口。这些常驻线程可以持续进行任务处理,以保证系统的基本运行能力。当任务数量超过了常驻核心数时,线程池会将多余的任务放入任务队列中等待处理。此时,线程池会根据需要动态地创建新的线程来处理任务,以满足系统的负载需求。这些新创建的线程可以在任务处理完成后立即销毁,从而节省系统资源。

线程池能够同时执行的最大线程数相当于银行的所有窗口数量。当任务队列中的任务超过了线程池能够处理的数量时,多余的任务将会被排队等待。这个过程就类似于银行的候客区,等待着客户能够进入银行。当线程池中的所有线程都在处理任务时,如果有新的任务需要处理,这些任务就会被放入任务队列中,等待空闲的线程来处理。

线程池的一个重要特性就是它能够处理突发性的任务请求。当有大量的任务需要同时处理时,线程池可以动态地创建新的线程,以满足系统的需求。如果线程池中线程的数量超过了最大限制,那么多余的任务就会被放入任务队列中等待。这个过程就类似于银行的窗口数量不足时,银行会临时加开窗口来处理客户。

还有一个重要特性就是线程池的鲁棒性。线程池的鲁棒性是指它在面对各种异常情况时的稳定性和可靠性。线程池可以高效地管理和调度大量的线程,可以根据实际情况调整线程池的大小和其他参数,使得系统在高并发场景下依然能够稳定运行。同时,线程池还具备一定的容错能力,可以在单个线程出现异常或崩溃时自动重新创建线程,避免因为单个线程的问题导致整个系统崩溃。

比如说,小明是一家工厂的生产经理,他需要确保每天工厂生产线的所有设备都在正常运行,并及时处理任何可能导致设备故障的异常情况。他决定使用线程池的思想来管理工厂的设备。他先定义了一个线程池的大小和其他相关参数,然后将每个设备的运行状态都放入线程池中的任务队列中,让线程池自动管理和调度这些任务。如果某个设备出现异常,例如机器过热,线程池会自动分配其他线程来处理这个问题,而不会对整个任务队列造成影响。这样,小明可以高效地处理工厂设备的异常情况,确保工厂稳定运行,提高了工厂的生产效率。

线程池需要能够处理各种异常情况,比如线程执行过程中出现了异常、线程执行时间过长等等。这种情况就类似于银行窗口工作出现问题时,银行需要及时处理故障,防止系统崩溃。

在处理任务时,线程池还需要考虑线程的优先级问题。线程池可以根据任务的优先级来动态地调整线程的优先级,以保证任务能够按照优先级依次被处理。这个过程就类似于银行对客户进行优先级排序,以保证优先处理重要客户的需求。

线程池的优化也是一个不断迭代的过程。对于线程池的设计来说,需要考虑到很多方面的因素,比如线程的创建和销毁、任务的调度和分配、线程的安全性和性能问题等等。只有综合考虑这些因素,才能够设计出高效、稳定、可靠的线程池系统。

总之,线程池是一种非常重要的多线程编程技术,能够有效地提高系统的运行效率和性能。在实际应用中,我们需要根据具体的需求来对线程池进行优化和改进,以满足不同的系统需求。


相关文章
|
1月前
|
并行计算 Java 数据处理
SpringBoot高级并发实践:自定义线程池与@Async异步调用深度解析
SpringBoot高级并发实践:自定义线程池与@Async异步调用深度解析
162 0
|
25天前
|
安全
List并发线程安全问题
【10月更文挑战第21天】`List` 并发线程安全问题是多线程编程中一个非常重要的问题,需要我们认真对待和处理。只有通过不断地学习和实践,我们才能更好地掌握多线程编程的技巧和方法,提高程序的性能和稳定性。
130 59
|
4天前
|
安全 Java 开发者
Java 多线程并发控制:深入理解与实战应用
《Java多线程并发控制:深入理解与实战应用》一书详细解析了Java多线程编程的核心概念、并发控制技术及其实战技巧,适合Java开发者深入学习和实践参考。
|
3月前
|
Java 开发者
解锁并发编程新姿势!深度揭秘AQS独占锁&ReentrantLock重入锁奥秘,Condition条件变量让你玩转线程协作,秒变并发大神!
【8月更文挑战第4天】AQS是Java并发编程的核心框架,为锁和同步器提供基础结构。ReentrantLock基于AQS实现可重入互斥锁,比`synchronized`更灵活,支持可中断锁获取及超时控制。通过维护计数器实现锁的重入性。Condition接口允许ReentrantLock创建多个条件变量,支持细粒度线程协作,超越了传统`wait`/`notify`机制,助力开发者构建高效可靠的并发应用。
90 0
|
16天前
|
存储 设计模式 分布式计算
Java中的多线程编程:并发与并行的深度解析####
在当今软件开发领域,多线程编程已成为提升应用性能、响应速度及资源利用率的关键手段之一。本文将深入探讨Java平台上的多线程机制,从基础概念到高级应用,全面解析并发与并行编程的核心理念、实现方式及其在实际项目中的应用策略。不同于常规摘要的简洁概述,本文旨在通过详尽的技术剖析,为读者构建一个系统化的多线程知识框架,辅以生动实例,让抽象概念具体化,复杂问题简单化。 ####
|
1月前
|
Java
【编程进阶知识】揭秘Java多线程:并发与顺序编程的奥秘
本文介绍了Java多线程编程的基础,通过对比顺序执行和并发执行的方式,展示了如何使用`run`方法和`start`方法来控制线程的执行模式。文章通过具体示例详细解析了两者的异同及应用场景,帮助读者更好地理解和运用多线程技术。
29 1
|
3月前
|
算法 Java
JUC(1)线程和进程、并发和并行、线程的状态、lock锁、生产者和消费者问题
该博客文章综合介绍了Java并发编程的基础知识,包括线程与进程的区别、并发与并行的概念、线程的生命周期状态、`sleep`与`wait`方法的差异、`Lock`接口及其实现类与`synchronized`关键字的对比,以及生产者和消费者问题的解决方案和使用`Condition`对象替代`synchronized`关键字的方法。
JUC(1)线程和进程、并发和并行、线程的状态、lock锁、生产者和消费者问题
|
2月前
|
网络协议 C语言
C语言 网络编程(十四)并发的TCP服务端-以线程完成功能
这段代码实现了一个基于TCP协议的多线程服务器和客户端程序,服务器端通过为每个客户端创建独立的线程来处理并发请求,解决了粘包问题并支持不定长数据传输。服务器监听在IP地址`172.17.140.183`的`8080`端口上,接收客户端发来的数据,并将接收到的消息添加“-回传”后返回给客户端。客户端则可以循环输入并发送数据,同时接收服务器回传的信息。当输入“exit”时,客户端会结束与服务器的通信并关闭连接。
|
2月前
|
数据采集 消息中间件 并行计算
进程、线程与协程:并发执行的三种重要概念与应用
进程、线程与协程:并发执行的三种重要概念与应用
60 0
|
2月前
|
C语言
C语言 网络编程(九)并发的UDP服务端 以线程完成功能
这是一个基于UDP协议的客户端和服务端程序,其中服务端采用多线程并发处理客户端请求。客户端通过UDP向服务端发送登录请求,并根据登录结果与服务端的新子线程进行后续交互。服务端在主线程中接收客户端请求并创建新线程处理登录验证及后续通信,子线程创建新的套接字并与客户端进行数据交换。该程序展示了如何利用线程和UDP实现简单的并发服务器架构。