【JUC基础】12. 线程池(一)

1、前言

我们知道多线程的使用,是为了最大限度发挥现代多核处理器的计算能力,提高系统的吞吐量和性能。但是如果不加以控制和管理,随意使用多线程,对系统性能反而会有不利的影响。线程数量和系统CPU资源是息息相关的,随意使用甚至可能会耗尽系统CPU资源和内存资源。

2、池化技术

为了应对多线程管理和控制的问题,引入池化技术。所谓池化技术,顾名思义就是造一个池子,讲需要管理的东西交给池子管理,而用完之后就放回池子。就像小孩子的玩具收纳箱,玩玩具的时候从收纳箱中拿出玩具,玩完之后一定要教小孩子将玩具放回玩具收纳箱中。池化技术通过优化资源的分配效率,从来达到性能的调优。

其实在Java编程中,池化技术不仅仅在多线程中使用这种方式,其他地方也同样用到了池化技术。如:数据连接池,对象池,内存池等等。

3、为什么要用线程池

前面基础多少讲到了为什么使用线程池的原因。这里详细说明以下几个原因,由ChatGPT来总结一下:

在多线程编程中,频繁地创建和销毁线程是一项昂贵的操作。因此,使用线程池来管理线程的创建、复用和销毁是一种有效的方式。 以下是几个原因解释为什么要使用线程池:

  1. 降低资源消耗:线程的创建和销毁需要消耗系统资源,如内存和CPU。使用线程池可以重用线程,避免频繁创建和销毁线程,从而降低了资源消耗。
  2. 提高系统响应性:线程池能够提高系统的并发能力和响应性。通过合理地配置线程池的大小,可以同时执行多个任务,提高系统的吞吐量和响应时间。
  3. 任务调度和线程复用:线程池可以管理和调度任务的执行。它维护一组线程,可以根据任务的到达顺序和优先级来选择合适的线程执行任务,避免任务争抢和冲突。同时,线程池中的线程可以被重复利用来执行多个任务,避免了频繁创建线程的开销。
  4. 控制并发线程数量:通过设置线程池的大小和任务队列的容量,可以限制并发执行的线程数量,防止系统资源被过度占用,从而提高系统的稳定性和可靠性。
  5. 简化线程编程:使用线程池可以将任务的提交和执行解耦,简化了线程编程的复杂性。开发人员只需关注任务的实现和提交,无需手动创建和管理线程,从而降低了出错的概率。

3.1、线程池优点

线程池优点很明显,上面提到为什么要使用线程池的几个原因就是对应的优点,这里不赘述。

3.2、线程池缺点

线程池的缺点也很明显:

  1. 资源占用:线程池本身会占用一定的系统资源,包括内存和CPU。如果线程池的大小设置不合理,可能会导致资源浪费或不足的问题。
  2. 线程泄露:如果没有正确地关闭线程池,或者任务执行过程中出现异常导致线程无法正常释放,可能会导致线程泄露,进而影响系统性能。
  3. 需要合理配置:线程池的性能和效果受到配置参数的影响,需要根据具体场景合理配置线程池的大小、任务队列的容量等参数,否则可能会影响系统的性能和响应性。
  4. 难以处理长时间任务:线程池主要适用于短时间的任务处理,如果任务执行时间过长,可能会导致线程池中的线程被长时间占用,影响其他任务的执行。

4、如何使用线程池

最简单的线程池使用方法:

public class ThreadPoolTest {

    public static void main(String[] args) {
        // 创建一个固定大小的线程池,大小为3
        ExecutorService executorService = Executors.newFixedThreadPool(3);

        // 提交任务给线程池执行
        for (int i = 0; i < 10; i++) {
            // 执行提交任务
            executorService.execute(() -> {
                // ......
            });
        }

        // 关闭线程池
        executorService.shutdown();
    }
}
  1. 通过Executors.newFixedThreadPool(3)方法创建了一个线程池,该线程池固定线程数量为3;
  2. 使用executorService.execute()方法执行向线程池内提交的线程任务;
  3. 执行完后,通过executorService.shutdown();关闭线程池资源;

通过简单的线程池使用方式,我们就完成了基本的线程池操作。线程池会自动管理线程的创建和销毁,以及任务的调度和执行,帮我们简化了多线程编程的复杂性。

5、JUC线程池

扫描二维码关注公众号,回复: 15492202 查看本文章

5.1、Executor

Executor 线程池顶级接口,类似一个线程池工厂。接口中只有一个execute()方法,接收Runnable类型。注意这里返回值类型是void。

5.2、ExecutorService

ExecutorService继承自Executor接口,添加了关闭线程池以及等待中断等方法。同时添加了submit来提交线程任务,除了接收Runnable以外,还可以接收Callable类型,也增加了返回值。

5.3、AbstractExecutorService

AbstractExecutorService是实现ExecutorService接口的抽象类。默认实现了个别如submit方法等。

5.4、ScheduledExecutorService

该类是为了实现带有定时器功能的线程池。ScheduledExecutorService也是一个接口。包含了定时和延迟处理的方法。

5.5、ThreadPoolExecutor方法参数

ThreadPoolExecutor重点看这个类。ThreadPoolExecutor是JUC中提供的默认线程池实现类。提供了丰富的配置选项和线程池管理功能。

提供了4个可选配置的构造函数:

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, 
                         long keepAliveTime, TimeUnit unit, 
                         BlockingQueue<Runnable> workQueue)
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, 
                          long keepAliveTime, TimeUnit unit, 
                          BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler)
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
                          long keepAliveTime, TimeUnit unit,
                          BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory)
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
                          long keepAliveTime, TimeUnit unit,
                          BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)

我们重点关注其中的几个参数:corePoolSize,maximumPoolSize,keepAliveTime,workQueue,threadFactory,handler。

5.5.1、corePoolSize

核心线程数。指线程池中始终保持的线程数量,就算他们处于空闲状态,也不会被销毁。而一直存活的最小线程数量。

5.5.2、maximumPoolSize

最大线程数。指线程池中允许的最大线程数量。当maximumPoolSize的数量大于corePoolSize时,多的那部分空闲状态下的线程,会再超过一定时间后被销毁,只保留corePoolSize的核心线程数。

5.5.3、keepAliveTime

非核心线程保持空闲的时间。如果超过这个时间,多的那部分空闲状态下的线程就会被销毁。可以通过unit设置时间单位。

5.5.4、BlockingQueue

任务队列,阻塞队列。当前并发执行的线程数与系统资源有关。当你设置了大于当前系统可负载的线程数量时,多的那部分自然要进行等待,从而进入等待队列。

当线程池中线程数量达到corePoolSize时,且都处于运行状态,这时候后续提交的线程任务会进入到缓存阻塞队列中,等待执行。这个缓存阻塞队列也就是workQueue。

JUC中提供的BlockingQueue有以下几种:

  1. ArrayBlockingQueue:由数组实现的有界阻塞队列。需要指定队列的容量大小。当队列已满时,添加任务的操作将被阻塞,直到队列中有空位。ArrayBlockingQueue适用于固定大小的线程池,可以控制线程池中的最大任务数。
  2. LinkedBlockingQueue:由链表实现的可选有界或无界阻塞队列。如果创建LinkedBlockingQueue时没有指定容量大小,那么它将是一个无界队列,可以无限制地添加任务。如果指定了容量大小,它将成为一个有界队列。当队列已满时,添加任务的操作将被阻塞。LinkedBlockingQueue适用于任务数比较大且变化较大的场景。
  3. SynchronousQueue:一个没有缓冲区的阻塞队列。每个插入操作必须等待一个相应的删除操作,反之亦然。SynchronousQueue适用于任务直接交付给线程执行的场景,可以有效地避免任务的排队和缓冲。
  4. PriorityBlockingQueue:支持优先级排序的无界阻塞队列。元素按照比较器或元素的自然顺序进行排序。PriorityBlockingQueue适用于需要按照优先级顺序处理任务的场景。

注:当使用了无界队列后,maximumPoolSize会失效。

这些BlockingQueue的区别主要在于容量限制、阻塞特性和元素排序。根据具体的需求和场景,选择合适的BlockingQueue可以提高线程池的性能和效率。

5.5.5、threadFactory

线程工厂。用于创建线程的工厂类。可以通过设置线程工厂来自定义线程的创建方式,例如设置线程名称、线程优先级等

5.5.6、RejectedExecutionHandler

拒绝策略。用于处理无法接收的任务。当线程池已满且任务无法提交时,会触发拒绝策略来处理这些任务。

JUC提供的RejectedExecutionHandler有以下几种:

  1. AbortPolicy(默认策略):该策略会直接抛出RejectedExecutionException异常,阻止任务的提交。
  2. CallerRunsPolicy:当线程池无法接收任务时,会将任务返回给调用者执行。也就是说,由提交任务的线程来执行该任务。这样可以降低任务提交速度,但可能会影响调用线程的性能。
  3. DiscardPolicy:该策略会默默丢弃无法接收的任务,没有任何提示和异常。这可能导致任务的丢失,潜在的风险需要注意。
  4. DiscardOldestPolicy:当线程池无法接收任务时,会丢弃队列中最旧的任务,然后尝试再次提交任务。这样可以保留较新的任务,但可能会丢失一些较旧的任务。

这些拒绝策略在处理无法接收的任务时具有不同的行为,可以根据具体的需求和业务场景选择合适的策略。需要根据任务的重要性、丢失任务的风险以及业务需求来综合考虑选择合适的拒绝策略。

5.6、手动创建一个线程池

private final static ThreadPoolExecutor threadPoolExecutor;

static {
    // 这里利用hutool提供的ThreadFactoryBuilder,创建一个线程池工厂,并配置线程名称前缀
    // ThreadFactory是个接口,也可以自定义实现
    ThreadFactory threadFactory = ThreadFactoryBuilder.create().setNamePrefix("common-thread-pool-").build();
    threadPoolExecutor = new ThreadPoolExecutor(

            // 核心线程数为7,
            // 通常IO密集型的可以配置为2 * cpu数量
            // CPU密集型的可以配置为 cpu数量 + 1
            7,
            
            // 最大线程数量20个
            20,
            
            // 空闲等待时间,1分钟
            // 超过1分钟,多余的空闲线程会被销毁
            1 * 60,
            
            // 空闲等待时间,单位
            TimeUnit.SECONDS,
            
            // 有界等待队列,固定长度为50
            // 如果使用无界队列,需要考虑内存占用问题
            new ArrayBlockingQueue(50),
            
            // 线程工厂
            threadFactory,
            
            // 拒绝策略
            new ThreadPoolExecutor.AbortPolicy());
}

6、小结

到这里,基本交代了线程池的一些基础概念,以及关于线程池的一些基础使用。后面的章节会讲到线程池的几个实现类,以及简单的场景使用案例。

持续更新中......

猜你喜欢

转载自blog.csdn.net/p793049488/article/details/130961086