[Java并发]线程池

ThreadPoolExecutor tpe = new ThreadPoolExecutor(
    10,
    20,
    1L,
    TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(100),
    new ThreadPoolExecutor.CallerRunsPolicy());

线程池参数的含义

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

这 7 个参数分别是:

  • corePoolSize:核心线程数。
  • maximumPoolSize:最大线程数。
  • keepAliveTime:空闲线程存活时间。
  • TimeUnit:时间单位。
  • BlockingQueue:线程池任务队列。
  • ThreadFactory:创建线程的工厂。
  • RejectedExecutionHandler:拒绝策略。

corePoolSize

核心线程数:是指线程池中长期存活的线程数。
这就好比古代大户人家,会长期雇佣一些“长工”来给他们干活,这些人一般比较稳定,无论这一年的活多活少,这些人都不会被辞退,都是长期生活在大户人家的。

maximumPoolSize

最大线程数:线程池允许创建的最大线程数量,当线程池的任务队列满了之后,可以创建的最大线程数。
这是古代大户人家最多可以雇佣的人数,比如某个节日或大户人家有人过寿时,因为活太多,仅靠“长工”是完不成任务,这时就会再招聘一些“短工”一起来干活,这个最大线程数就是“长工”+“短工”的总人数,也就是招聘的人数不能超过 maximumPoolSize。

最大线程数 maximumPoolSize 的值不能小于核心线程数 corePoolSize,否则在程序运行时会报 IllegalArgumentException 非法参数异常

keepAliveTime

空闲线程存活时间,当线程池中没有任务时,会销毁一些线程,销毁的线程数=maximumPoolSize(最大线程数)-corePoolSize(核心线程数)。
还是以大户人家为例,当大户人家比较忙的时候就会雇佣一些“短工”来干活,但等干完活之后,不忙了,就会将这些“短工”辞退掉,而 keepAliveTime 就是用来描述没活之后,短工可以在大户人家待的(最长)时间。

TimeUnit

时间单位:空闲线程存活时间的描述单位,此参数是配合参数 3 使用的。
参数 3 是一个 long 类型的值,比如参数 3 传递的是 1,那么这个 1 表示的是 1 天?还是 1 小时?还是 1 秒钟?是由参数 4 说了算的。
TimeUnit 有以下 7 个值:

  • TimeUnit.DAYS:天
  • TimeUnit.HOURS:小时
  • TimeUnit.MINUTES:分
  • TimeUnit.SECONDS:秒
  • TimeUnit.MILLISECONDS:毫秒
  • TimeUnit.MICROSECONDS:微妙
  • TimeUnit.NANOSECONDS:纳秒

BlockingQueue

阻塞队列:线程池存放任务的队列,用来存储线程池的所有待执行任务。
它可以设置以下几个值:

  • ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
  • LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。
  • SynchronousQueue:一个不存储元素的阻塞队列,即直接提交给线程不保持它们。
  • PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
  • DelayQueue:一个使用优先级队列实现的无界阻塞队列,只有在延迟期满时才能从中提取元素。
  • LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。与SynchronousQueue类似,还含有非阻塞方法。
  • LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
    比较常用的是 LinkedBlockingQueue,线程池的排队策略和 BlockingQueue 息息相关。

ThreadFactory

线程工厂:线程池创建线程时调用的工厂方法,通过此方法可以设置线程的优先级、线程命名规则以及线程类型(用户线程还是守护线程)等。
线程工厂的使用示例如下:

public static void main(String[] args) {
    // 创建线程工厂
    ThreadFactory threadFactory = new ThreadFactory() {
        @Override
        public Thread newThread(Runnable r) {
            // 创建线程池中的线程
            Thread thread = new Thread(r);
            // 设置线程名称
            thread.setName("Thread-" + r.hashCode());
            // 设置线程优先级(最大值:10)
            thread.setPriority(Thread.MAX_PRIORITY);
            //......
            return thread;
        }
    };
    // 创建线程池
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 10, 0,
                                                                   TimeUnit.SECONDS, new LinkedBlockingQueue<>(),
                                                                   threadFactory); // 使用自定义的线程工厂
    threadPoolExecutor.submit(new Runnable() {
        @Override
        public void run() {
            Thread thread = Thread.currentThread();
            System.out.println(String.format("线程:%s,线程优先级:%d",
                                             thread.getName(), thread.getPriority()));
        }
    });
}

RejectedExecutionHandler

拒绝策略:当线程池的任务超出线程池队列可以存储的最大值之后,执行的策略。
默认的拒绝策略有以下 4 种:

AbortPolicy:拒绝并抛出异常。
CallerRunsPolicy:使用当前调用的线程来执行此任务。
DiscardOldestPolicy:抛弃队列头部(最旧)的一个任务,并执行当前任务。
DiscardPolicy:忽略并抛弃当前任务。
线程池的默认策略是 AbortPolicy 拒绝并抛出异常。

总结

  • corePoolSize:核心线程数,线程池正常情况下保持的线程数,大户人家“长工”的数量。
  • maximumPoolSize:最大线程数,当线程池繁忙时最多可以拥有的线程数,大户人家“长工”+“短工”的总数量。
  • keepAliveTime:空闲线程存活时间,没有活之后“短工”可以生存的最大时间。
  • TimeUnit:时间单位,配合参数 3 一起使用,用于描述参数 3 的时间单位。
  • BlockingQueue:线程池的任务队列,用于保存线程池待执行任务的容器。
  • ThreadFactory:线程工厂,用于创建线程池中线程的工厂方法,通过它可以设置线程的命名规则、优先级和线程类型。
  • RejectedExecutionHandler:拒绝策略,当任务量超过线程池可以保存的最大任务数时,执行的策略。

线程池的工作过程

  1. 最开始,线程池中的没有线程在运行
  2. 向线程池提交任务,
  3. 线程池不断创建核心线程来执行这些任务,即使有其他空闲线程能够执行新来的任务, 也会继续创建线程,直到核心线程数到达上限
  4. 继续向线程池提交任务
  5. 新提交的任务会保存在阻塞任务队列,直到阻塞任务队列到达上限
  6. 继续向线程池提交任务,
  7. 线程池创建非核心线程来执行这些任务,直到总线程数到达上限
  8. 继续向线程池提交任务,
  9. 线程池采用四种拒绝策略
    1. 直接抛出异常,
    2. 直接拒绝执行,
    3. 删掉阻塞队列最前面的任务,让当前任务上去执行,
    4. 让调用者自己去执行

参考资料1

参考资料2

关闭线程池

关闭线程池是一个重要的操作,它确保线程池不再接受新的任务,并且在已提交的任务完成后,逐渐关闭所有线程。Java中的 ExecutorService 接口提供了两个方法来关闭线程池:

  1. shutdown() 方法:

    • 调用 shutdown() 方法会平缓地关闭线程池。它将禁止新的任务被提交,但会等待已提交的任务(包括正在执行和在队列中的任务)执行完成。
    • 一旦所有任务完成,线程池就会终止,且不再接受新的任务。
    executorService.shutdown();
    
  2. shutdownNow() 方法:

    • 调用 shutdownNow() 方法会尝试立即停止所有正在执行的任务,并停止等待执行的任务。这可能会导致一些任务被中断,因为它们可能正在执行一些阻塞操作。
    • shutdown() 不同,shutdownNow() 不会等待任务执行完成,而是立即返回一个包含尚未执行的任务的列表。
    List<Runnable> remainingTasks = executorService.shutdownNow();
    

一般来说,首选使用 shutdown() 方法,因为它提供了平缓的关闭过程,允许已提交的任务得到充分的执行时间。只有在需要尽快停止并清理线程池时,才考虑使用 shutdownNow() 方法。

需要注意的是,一旦线程池关闭,就不能再向其提交新的任务。如果需要重新使用线程池,必须重新创建一个新的线程池实例。

为什么线程池不允许使用Executors去创建? 推荐方式是什么?

线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 说明:Executors各个方法的弊端:

  • newFixedThreadPool和newSingleThreadExecutor:   主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。
  • newCachedThreadPool和newScheduledThreadPool:   主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。

原文链接:https://pdai.tech/md/java/thread/java-thread-x-juc-executor-ThreadPoolExecutor.html

posted @ 2024-07-03 20:43  Duancf  阅读(1)  评论(0编辑  收藏  举报