多线程之四:线程池

目录

一、线程池概述

1. 什么是线程池

2. 使用场景

3. 线程池UML架构

(1)Executor

(2)ExecutorService

(3)Executors

二、Executors创建线程的几种方式

三、 ThreadPoolExecutor应用&源码剖析

3.1 线程池参数

3.2 参数执行流程

3.3 线程池提交任务的两种方式

3.4 ThreadFactory(线程工厂)

3.5 任务阻塞队列

4.7 线程池的拒绝策略

4.8 调度器的钩子方法

4.9 ThreadPoolExecutor源码剖析

4.10 线程池参数设计★★★★★

4.11 线程池处理任务的核心流程★★★★★

四、ScheduleThreadPoolExecutor应用&源码

4.1 ScheduleThreadPoolExecutor概述

4.2 ScheduleThreadPoolExecutor应用


 

一、线程池概述

Java线程池实现原理及其在美团业务中的实践 - 美团技术团队 ★★★★★

线程池的使用场景-CSDN博客

线程池整理汇总-CSDN博客

线程池详解-CSDN博客

Java线程池(超详细)-CSDN博客

百度安全验证icon-default.png?t=N7T8https://baijiahao.baidu.com/s?id=1677095792474889153&wfr=spider&for=pc

1. 什么是线程池

开发中,为了执行效率,一些任务需要多线程去完成,而线程本身的创建并销毁往往需要大量的开销,因而引入线程池的概念。一种池化思想。

2. 使用场景

略。

3. 线程池UML架构

 分析:

(1)Executor

Executor是最顶层的接口,它只定义了一个方法void execute(Runnable command),没有返回值

An object that executes submitted Runnable tasks. This interface provides a way of decoupling task submission from the mechanics of how each task will be run, including details of thread use, scheduling, etc. An Executor is normally used instead of explicitly creating threads. 

执行已提交的 Runnable 任务的对象。此接口提供了一种将任务提交与每个任务的运行机制分离(解耦)的方法,包括线程使用、调度等的详细信息。通常使用 Executor 而不是显式创建线程:executor.execute(new RunnableTask()); // 异步执行

(2)ExecutorService

ExecutorService也是接口,继承自Executor接口。它扩展了Executor的功能,

An Executor that provides methods to manage termination and methods that can produce a Future for tracking progress of one or more asynchronous tasks.

        一个 Executor,它提供用于管理终止的方法,以及可以生成 Future 的方法,用于跟踪一个或多个异步任务的进度。
An ExecutorService can be shut down, which will cause it to reject new tasks. Two different methods are provided for shutting down an ExecutorService. The shutdown method will allow previously submitted tasks to execute before terminating, while the shutdownNow method prevents waiting tasks from starting and attempts to stop currently executing tasks. Upon termination, an executor has no tasks actively executing, no tasks awaiting execution, and no new tasks can be submitted. An unused ExecutorService should be shut down to allow reclamation of its resources.

        可以关闭 ExecutorService,这将导致它拒绝新任务。提供了两种不同的方法来关闭 ExecutorService。shutdown 方法将允许以前提交的任务在终止之前执行,而 shutdownNow 方法阻止等待任务启动并尝试停止当前正在执行的任务。终止后,执行者没有正在执行的任务,没有等待执行的任务,也不能提交新任务。应关闭未使用的 ExecutorService,以允许回收其资源。
Method submit extends base method Executor.execute(Runnable) by creating and returning a Future that can be used to cancel execution and/or wait for completion. Methods invokeAny and invokeAll perform the most commonly useful forms of bulk execution, executing a collection of tasks and then waiting for at least one, or all, to complete. (Class ExecutorCompletionService can be used to write customized variants of these methods.)

        方法 submit 通过创建并返回可用于取消执行和/或等待完成的 Future 来扩展基方法 Executor.execute(Runnable)。invokeAny 和 invokeAll 方法执行最常用的批量执行形式,执行一组任务,然后等待至少一个或所有任务完成。(类 ExecutorCompletionService 可用于编写这些方法的自定义变体。

    /**
     * Submits a value-returning task for execution and returns a
     * Future representing the pending results of the task. The
     * Future's {@code get} method will return the task's result upon
     * successful completion.
     *
     * @param task the task to submit
     * @param <T> the type of the task's result
     * @return a Future representing pending completion of the task
     * @throws RejectedExecutionException if the task cannot be
     *         scheduled for execution
     * @throws NullPointerException if the task is null
     */
    <T> Future<T> submit(Callable<T> task);

<T> Future<T> submit(Callable<T> task);——提交一个返回值的任务以供执行,并返回一个 Future,表示该任务的挂起结果。Future 的 get 方法将在成功完成后返回任务的结果。

    /**
     * Submits a Runnable task for execution and returns a Future
     * representing that task. The Future's {@code get} method will
     * return {@code null} upon <em>successful</em> completion.
     */
    Future<?> submit(Runnable task);

 Future<?> submit(Runnable task);——提交一个 Runnable 任务进行执行,并返回表示该任务的 Future。成功完成后,Future 的 get 方法将返回 null。

    /**
     * Submits a Runnable task for execution and returns a Future
     * representing that task. The Future's {@code get} method will
     * return the given result upon successful completion.
     *
     * @param task the task to submit
     * @param result the result to return
     * @param <T> the type of the result
     */
    <T> Future<T> submit(Runnable task, T result);

 <T> Future<T> submit(Runnable task, T result);——提交一个 Runnable 任务进行执行,并返回表示该任务的 Future。Future's get 方法将在成功完成后返回给定的结果。

(3)Executors

Executors类,提供了一系列工厂方法用于创建线程池,返回的线程池都实现了ExecutorService接口。主要用于提供线程池相关的操作。例如:

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
            0L, TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue<Runnable>());
}
  • public static ExecutorService newFiexedThreadPool(int Threads): 创建固定数目线程的线程池。
  • public static ExecutorService newCachedThreadPool():创建一个可缓存的线程池,调用execute 将重用以前构造的线程(如果线程可用)。如果没有可用的线程,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。
  • public static ExecutorService newSingleThreadExecutor():创建一个单线程化的Executor。
  • public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize): 创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。

 参考:java并发编程:Executor、Executors、ExecutorService_executor executors-CSDN博客

二、Executors创建线程的几种方式

Executors类,提供了一系列工厂方法用于创建线程池生产上这种方式一般不用,因为参数配置不够灵活。具体方式见上面内容。

/**
 * Factory and utility methods for {@link Executor}, {@link
 * ExecutorService}, {@link ScheduledExecutorService}, {@link
 * ThreadFactory}, and {@link Callable} classes defined in this
 * package. This class supports the following kinds of methods:
 * 此包中定义的 Executor、ExecutorService、ScheduledExecutorService、ThreadFactory 和 Callable 类的工厂和实用工具方法。此类支持以下类型的方法:
    1.创建和返回 ExecutorService 的方法,这些方法设置了常用的配置设置。
    2.创建并返回使用常用配置设置设置的 ScheduledExecutorService 的方法。
    3.创建并返回“包装”的 ExecutorService 的方法,该方法通过使特定于实现的方法无法访问来禁用重新配置。
    4.创建并返回将新创建的线程设置为已知状态的 ThreadFactory 的方法。
    5.从其他类似闭包的表单中创建并返回 Callable 的方法,因此它们可以用于需要 Callable 的执行方法。
 *   <li> Methods that create and return an {@link ExecutorService}
 *        set up with commonly useful configuration settings.
 *   <li> Methods that create and return a {@link ScheduledExecutorService}
 *        set up with commonly useful configuration settings.
 *   <li> Methods that create and return a "wrapped" ExecutorService, that
 *        disables reconfiguration by making implementation-specific methods
 *        inaccessible.
 *   <li> Methods that create and return a {@link ThreadFactory}
 *        that sets newly created threads to a known state.
 *   <li> Methods that create and return a {@link Callable}
 *        out of other closure-like forms, so they can be used
 *        in execution methods requiring {@code Callable}.
 *
 * @since 1.5
 * @author Doug Lea
 */
public class Executors {

郑金维老师讲的,还是记录一下:

(1)newFixedThreadPool:懒加载,在构建之初,线程并没有构建出来,而是随着任务的提交而创建出来的。任务会被放到LinkedBlockingQueue无界队列中存放,等待线程从LinkedBlockingQueue中去take出任务,然后执行。

(2)newSingleThreadExecutor:这个线程池看名字就知道是单例线程池,线程池中只有一个工作线程在处理任务,如果业务涉及到顺序消费,可以采用。

(3)newCachedThreadPool:当第一次提交任务到线程池时,会直接构建一个工作线程,当后续提升任务时,如果没有线程是空闲的,那么就继续构建工作线程去执行。 处理完所有任务后60秒没有可处理任务时,会结束。最大的特点是:不会等待,即只要有任务进来就会用空闲状态的线程或者构建新的线程去处理。

(4)newScheduledThreadPool:延迟执行,周期执行,基于DelayQueue实现的延迟执行。

(5)newWorkStealingPool:是基于ForkJoinPool构建出来的。

三、 ThreadPoolExecutor应用&源码剖析

3.1 线程池参数

public ThreadPoolExecutor(
    int corePoolSize,           // 核心工作线程(当前任务执行结束后,不会被销毁)
    int maximumPoolSize,        // 最大工作线程(代表当前线程池中,一共可以有多少个工作线程)
    long keepAliveTime,         // 非核心工作线程在阻塞队列位置等待的时间
    TimeUnit unit,              // 非核心工作线程在阻塞队列位置等待时间的单位
    BlockingQueue<Runnable> workQueue,   // 任务在没有核心工作线程处理时,任务先扔到阻塞队列中
    ThreadFactory threadFactory,         // 构建线程的线程工作,可以设置thread的一些信息
    RejectedExecutionHandler handler) {  // workQueue 队列已满,总线程数又达到了maximumPoolSize,执行拒绝策略。
    // 初始化线程池的操作
}

先看一段程序:

 

(1)corePoolSize 核心线程数

(2)maximumPoolSize 最大线程数

(3)BlockingQueue<Runnable> workQueue 任务阻塞队列。维护着等待执行的 Runnable 异步任务。当所有的核心线程都在干活时,新添加的任务会被添加到这个队列中等待处理,如果队列满了,则新建非核心线程执行任务。

(4)long keepAliveTime 非核心线程闲置超时时长;一个非核心线程,如果闲置状态的时长超过这个参数所设定的时长,就会被销毁掉。

(5)TimeUnit unit 它是keepAliveTime 的单位,TimeUnit是一个枚举类型。

(6)ThreadFactory threadFactory 线程工厂。创建线程的方式,实现方法public Thread newThread(Runnable r)即可。

(7)RejectedExecutionHandler handler 拒绝策略。workQueue 队列已满,总线程数又达到了 maximumPoolSize,执行拒绝策略。
 

3.2 参数执行流程

1. 线程数量未达到corePoolSize,则新建一个线程(核心线程)执行任务。
2. 线程数量达到了corePoolSize,则将任务移入队列workQueue等待
3. 队列已满,新建线程(非核心线程)执行任务。
4. 队列已满,总线程数又达到了maximumPoolSize,执行拒绝策略RejectedExecutionHandler抛出异常。
5. 非核心线程闲置状态时长超过keepAliveTime ,就会被销毁掉。

注意:

  • 核心线程数满了之后,再有工作任务过来时,会往阻塞队列中放;放满了再有任务过来时才会创建非核心线程哦!!!
  • 队列满了,总线程数又达到了maximumPoolSize,就会执行拒绝策略RejectedExecutionHandler哦。其实拒绝策略就是一个方法,官方提供了4种实现,我们也可以自定义拒绝策略,具体是重新把任务扔到队列,还是抛异常,还是什么都不做,就得根据业务区选择。

3.3 线程池提交任务的两种方式

1. execute方法

  • void execute(Runnable command):Executor接口方法,接收 Runnable 类型对象。

2. submit方法 

  • <T> Future<T> submit(Callable<T> task):ExecutorService接口方法,接收 Callable 类型对象,Callable入参允许任务返回值。
  • <T> Future<T> submit(Runnable task, T result):ExecutorService接口方法,接收 Runnable 类型对象。
  • Future<?> submit(Runnable task):ExecutorService接口方法,接收 Runnable 类型对象。

区别:

  • execute()方法只能接收 Runnable 类型的参数,而submit()方法可以接收 Callable、Runnable 两种类型的参数。
  • Callable 类型的任务是可以返回执行结果的,而 Runnable 类型的任务不可以返回执行结果。
  • submit()提交任务后会有返回值,而execute()没有。
  • submit()方便 Exception 处理
  • execute()方法在启动任务执行后,任务执行过程中可能发生的异常调用者并不关心。而通过submit()方法返回的 Future 对象(异步执行实例),可以进行异步执行过程中的异常捕获。
     

 扩展:Java线程池系列之execute和submit区别_线程池submit和execute-CSDN博客

3.4 ThreadFactory(线程工厂)

        Executors为线程池工厂类,用于快捷创建线程池(Thread Pool);

        ThreadFactory为线程工厂类,用于创建线程(Thread)

public interface ThreadFactory {

    Thread newThread(Runnable r);
}

 ThreadFactory是 Java 线程工厂接口,只有1个方法,调用ThreadFactory的唯一方法newThread()创建新线程时,可以更改所创建的新线程的名称、线程组、优先级、守护进程状态等。

3.5 任务阻塞队列

维护着等待执行的 Runnable 对象。一个线程从一个空的阻塞队列中获取元素时线程会被阻塞,直到阻塞队列中有了元素;当队列中有元素后,被阻塞的线程会自动被唤醒。

当所有的核心线程都在干活时,新添加的任务会被添加到这个队列中等待处理,如果队列满了,则新建非核心线程执行任务。常用的workQueue类型:

  • LinkedBlockingQueue:一个基于链表实现的阻塞队列,按 FIFO 排序任务,可以设置容量(有界队列),不设置容量则默认使用Integer.Max_VALUE作为容量(无界队列)。
  • ArrayBlockingQueue:一个数组实现的有界阻塞队列(有界队列),队列中的元素按 FIFO 排序,ArrayBlockingQueue在创建时必须设置大小。接收到任务的时候,如果没有达到 corePoolSize 的值,则新建线程(核心线程)执行任务,如果达到了,则入队等候,如果队列已满,则新建线程(非核心线程)执行任务,又如果总线程数到了 maximumPoolSize,并且队列也满了,则执行拒绝策略。
  • SynchronousQueue:这个队列接收到任务的时候,会直接提交给线程处理,而不保留它。
  • PriorityBlockingQueue:是具有优先级无界队列。
  • DelayQueue:队列内元素必须实现 Delayed 接口,这就意味着你传进去的任务必须先实现 Delayed 接口。这个队列接收到任务时,首先先入队,只有达到了指定的延时时间,才会执行任务。

4.7 线程池的拒绝策略

/**
 * Exception thrown by an {@link Executor} when a task cannot be
 * accepted for execution.
 *
 * @since 1.5
 * @author Doug Lea
 */
public class RejectedExecutionException extends RuntimeException {

1. AbortPolicy:无法处理任务时,直接抛出RejectedExecutionException异常。

2. DiscardPolicy:丢弃策略;新任务就会直接被丢掉,并且不会有任何异常抛出。

3. DiscardOldestPolicy:抛弃最老任务策略;将最早进入队列的任务抛弃,从队列中腾出空间,再尝试加入队列(一般队头元素最老)。

4. CallerRunsPolicy:调用者执行策略;新任务被添加到线程池时,如果添加失败,那么提交任务线程会自己去执行该任务,不会使用线程池中的线程去执行新任务。 适应场景:一般并发比较小,性能要求不高,不允许失败。但是,由于调用者自己运行任务,如果任务提交速度过快,可能导致程序阻塞,性能效率上必然的损失较大。

5. 自定义策略:像上面四种策略一样,实现RejectedExecutionHandler接口,重写内部的rejectedExecution(Runnable r, ThreadPoolExecutor executor);方法即可。

    static class MyRejectedPolicy<T extends List> implements RejectedExecutionHandler {
        public T t;

        public MyRejectedPolicy(T t) {
            this.t = t;
        }

        @Override
        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
            //被拒绝的任务写入List集合,当然也可以写入文件、数据库等等
            System.out.println(executor.getTaskCount());
            t.add(executor.getTaskCount());
        }
    }

6. 第三方实现的拒绝策略

  • dubbo中的线程拒绝策略:输出了一条警告级别的日志,输出当前线程堆栈详情,继续抛出拒绝执行异常。
  • Netty中的线程池拒绝策略:Netty中的实现很像JDK中的CallerRunsPolicy,舍不得丢弃任务。不同的是,CallerRunsPolicy是直接在调用者线程执行的任务。而 Netty是新建了一个线程来处理的。
  • activeMq中的线程池拒绝策略:activeMq中的策略属于最大努力执行任务型,当触发拒绝策略时,在尝试一分钟的时间重新将任务塞进任务队列,当一分钟超时还没成功时,就抛出异常。

小结:

四种拒绝策略是相互独立无关的,选择何种策略去执行,还得结合具体的业务场景。实际工作中,一般直接使用 ExecutorService 的时候,都是使用的默认的 defaultHandler ,也即 AbortPolicy 策略。

4.8 调度器的钩子方法

三个钩子方法存在于 ThreadPoolExecutor 类,这3个方法都是空方法,一般会在子类中重写。

  • protected void beforeExecute(Thread t, Runnable r) { }: 任务执行之前的钩子方法
  • protected void afterExecute(Runnable r, Throwable t) { }: 任务执行之后的钩子方法
  • protected void terminated() { }: 线程池终止时的钩子方法

 

4.9 ThreadPoolExecutor源码剖析

1. execute提交任务到线程池的核心方法,很重要,submit方法内部也是执行的execute方法;

2. addWorker中主要分成两大块去看

  • 第一块:校验线程池的状态以及工作线程个数

  • 第二块:添加工作线程并且启动工作线程

3. Worker对象主要包含了两个内容

  • 工作线程要执行任务

  • 工作线程可能会被中断,控制中断

4. runWorker就是让工作线程拿到任务去执行即可。并且在内部也处理了在工作线程正常结束和异常结束时的处理方案。

5. getTask方法,就在阻塞队列中获取任务

6. 线程池关闭的方法:

  • shutdown状态下,不会中断正在干活的线程,而且会处理阻塞队列中的任务;
  • shutdownNow方法,可以从RUNNING状态转变为STOP,不会处理任务。

4.10 线程池参数设计★★★★★

(1)线程池的核心参数无非就是:

  • corePoolSize:核心线程数

  • maximumPoolSize:最大线程数

  • workQueue:工作队列

线程池中提供了获取核心信息的get方法,同时也提供了动态修改核心属性的set方法。

(2)也可以采用一些开源项目提供的方式去做监控和修改,比如hippo4j就可以对线程池进行监控,而且可以和SpringBoot整合。

Github地址:GitHub - opengoofy/hippo4j: 📌 异步线程池框架,支持线程池动态变更&监控&报警,无需修改代码轻松引入。Asynchronous thread pool framework, support Thread Pool Dynamic Change & monitoring & Alarm, no need to modify the code easily introduced.

官方文档:简介 | Hippo4j

或者在gitee网站搜线程池监控,还有其他一些方案。

(3)参考:线程池中各个参数如何合理设置_线程池参数如何设置-CSDN博客

一般可以先这样设置,再监控调整:

  • CPU密集型:corePoolSize = CPU核数 + 1
  • IO密集型:corePoolSize = CPU核数 * 2

4.11 线程池处理任务的核心流程★★★★★

From郑金维老师。若必要,可以配合讲义中execute方法的那张详细的图看看

 

四、ScheduleThreadPoolExecutor应用&源码

4.1 ScheduleThreadPoolExecutor概述

从名字上就可以看出,当前线程池是用于执行定时任务的线程池。

Java比较早的定时任务工具是Timer类。但是Timer问题很多,串行的,不靠谱,会影响到其他的任务执行。

其实除了Timer以及ScheduleThreadPoolExecutor之外,正常在企业中一般会采用Quartz或者是SpringBoot提供的Schedule的方式去实现定时任务的功能。

ScheduleThreadPoolExecutor支持延迟执行以及周期性执行的功能。

4.2 ScheduleThreadPoolExecutor应用

定时任务线程池的有参构造:

public ScheduledThreadPoolExecutor(int corePoolSize,
                                   ThreadFactory threadFactory,
                                   RejectedExecutionHandler handler) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue(), threadFactory, handler);
}

可以看出ScheduleThreadPoolExecutor在构建时,直接调用了父类的构造方法,其父类就是ThreadPoolExecutor

首先ScheduleThreadPoolExecutor最多允许设置3个参数:

  • 核心线程数

  • 线程工厂

  • 拒绝策略

没有设置阻塞队列,以及最大线程数和空闲时间以及单位的位置。

阻塞队列设置的是DelayedWorkQueue,其实本质就是DelayQueue,一个延迟队列。DelayQueue是一个无界队列。所以最大线程数以及非核心线程的空闲时间是不需要设置的。

代码落地使用:

public static void main(String[] args) {
        //1. 构建定时任务线程池
        ScheduledThreadPoolExecutor pool = new ScheduledThreadPoolExecutor(
                5,
                new ThreadFactory() {
                    @Override
                    public Thread newThread(Runnable r) {
                        Thread t = new Thread(r);
                        return t;
                    }
                },
                new ThreadPoolExecutor.AbortPolicy()
        );
  
        //2. 应用ScheduledThreadPoolExecutor
        // 跟直接执行线程池的execute没啥区别
        pool.execute(() -> {
            System.out.println("execute");
        });
  
        // 指定延迟时间执行
        System.out.println(System.currentTimeMillis());
        pool.schedule(() -> {
            System.out.println("schedule");
            System.out.println(System.currentTimeMillis());
        },2, TimeUnit.SECONDS);
  
        // 指定第一次的延迟时间,并且确认后期的周期执行时间,周期时间是在任务开始时就计算
        // 周期性执行就是将执行完毕的任务再次社会好延迟时间,并且重新扔到阻塞队列
        // 计算的周期执行,也是在原有的时间上做累加,不关注任务的执行时长。
        System.out.println(System.currentTimeMillis());
        pool.scheduleAtFixedRate(() -> {
            System.out.println("scheduleAtFixedRate");
            System.out.println(System.currentTimeMillis());
        },2,3,TimeUnit.SECONDS);
  
  
    //        // 指定第一次的延迟时间,并且确认后期的周期执行时间,周期时间是在任务结束后再计算下次的延迟时间
        System.out.println(System.currentTimeMillis());
        pool.scheduleWithFixedDelay(() -> {
            System.out.println("scheduleWithFixedDelay");
            System.out.println(System.currentTimeMillis());
            try {
                Thread.sleep(4000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },2,3,TimeUnit.SECONDS);
    }

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值