线程池 Executor 最详细理解

小编写博客不容易,转载请注明出处
小编写博客不容易,转载请注明出处

前言:

对于线程池它起到的作用我相信大家都或多或少有所了解了。它本身核心的作用就是对线程有一个Pool 池化作用可以维护着多个线程,这些线程能够在我们整个程序的运行期间呢线程可以复用。线程本身虽然是轻量级的,但是线程创建、销毁过程也是需要消耗资源的。

正文

最顶层的是 Executor 这个接口,在jdk 1.5 版本引入进来的,它java.util.concurrent包中,它里面就唯独只有一个方法 execute,没有返回值
在这里插入图片描述

ExecutorService是Java对线程池定义的一个接口,它继承了父接口 Executor ,也是在jdk 1.5 版本引入进来的。它java.util.concurrent包中,它在 Executor 的基础上还有其他的一些方法:
shutdown、submit…
在这里插入图片描述
线程池在使用完之后务必要把它关闭掉,因为线程池创建的线程并不是守护线程,我们知道,当我们的 JVM 在运行的时候,什么时候会退出?就是没有用户线程再执行的时候,就会退出(即使这个时候有守护线程)。
假如我们的主线程,就是main线程执行完了之后,但是后台还有用户线程在执行的时候,我们的JVM是永远不会退出的(后续文章还会对此进行分析)

今天的主角就是:ThreadPoolExecutor

我们用到最多的就是ThreadPoolExecutor,ThreadPoolExecutor 间接地实现了 ExecutorService

它有一个原子整型变量 : AtomicInteger ctl
在这里插入图片描述
ctl有两层含义:

  • 表示我线程池自身的状态是什么
  • 表示我这个线程池中维护了多少个线程

Integer.SIZE =32 ,它头三位表示的是线程池的状态, 32-3=29 ,29表示的是线程池当中所被管理的线程的数量

它的私有的内部类:Worker

线程池当中有一个私有的内部内: worker 工作者线程,是基于AQS的实现。
在这里插入图片描述
关于AQS的话,我额外还有一篇文章是专门写 AQS的:链接地址

------------------------------------------红色的分割线----------------------------------------------------------
------------------------------------------红色的分割线----------------------------------------------------------
------------------------------------------红色的分割线----------------------------------------------------------

线程池的创建方式与工厂模式的应用

1.它有四种构造方法:
在这里插入图片描述
对于它的7个构造参数都是非常重要的…下文会继续讲解…

2.Executors 是一个工厂类,也可以看做为一个工具类,调用一些方法,创建一些特定的线程池对象。
在这里插入图片描述

Executors 其实它的本质就是调用线程池的第一种构建方式,只是它帮我们写好了一些特定的参数传进去仅此而已。
在这里插入图片描述
**建议:**对于中小型的程序来说,Executors 提供出来的这些工厂方法是够用了的。但是对于一些高并发,大型公司来说,工厂方法就不适用了。因为线程池的每一个参数,都是至关重要的,需要我们从实际的业务去出发,精心设定。

ThreadPoolExecutor 核心参数详解:

  • int corePoolSize: 线程池当中所一直维护的线程数量,如果线程处于任务空闲期间,那么该线程也并不会被回收掉

  • int maximumPoolSize: 线程池中所维护的线程数的最大的数量

  • long keepAliveTime: 超过了 corePoolSize 的线程再经过 keepAliveTime 时间后如果一直处于空闲状态,那么超过的这部分线程就会被回收掉

  • TimeUnit unit: 指的是 keepAliveTime 的时间单位

  • BlockingQueue workQueue: 向线程池锁提交的任务位于的 阻塞队列(FIFO),先放进队列的就先执行,后放进去的线程就后执行。BlockingQueue是一个接口,它的实现有多种方式。 用的多的就是 ArrayBlockingQueue 、 LinkedBlockingQueue、SynchronousQueue
    在这里插入图片描述

  • ThreadFactory threadFactory: 线程工厂,用于创建新的线程并被线程池所管理,默认的线程工厂所创建的线程都是用户线程且优先级为正常的优先级。ThreadFactory它是一个接口,实现类也有很多
    在这里插入图片描述
    其中 DefaultThreadFactory 是默认使用的工厂类
    在这里插入图片描述

这里还有一个涉及的2个小细节:
1.为什么 *poolNumber(池数量)*是 静态修饰的,threadNumber(线程数量) 不是静态的。

回答:因为一个程序里面可以有多个线程池对象,而为了达到对整个程序线程池对象的计数统计,就需要对 poolNumber 进行 static 修饰(只有一份),而一个线程池对象里面的线程数量是只针对一个线程池而言的,所以它不需要被 static 修饰

2.红框圈的这个代码: t.setDaemon(false);

如果检测到创建的线程是守护线程,就把它转换成用户线程。所以通过线程池创建的线程,都是用户线程。这就论证了上文说的,为什么我们使用完了 线程池,一定要调用 shutdown、shutdownNow方法把线程池关闭。就是为了防止 JVM退出不了

  • RejectedExecutionHandler handler: 表示当线程池中的线程都忙于执行任务且阻塞队列已经满了的情况下,新到来的任务该如何被对待和处理

它有四种拒绝实现策略:

  1. AbortPolicy:直接放弃,什么都不处理,直接抛出一个运行时异常。

  2. DiscardPolicy: 默默地丢弃提交的任务,什么都不做且不抛出任何异常。(Does nothing, which has the effect of discarding task r)

  3. DiscardOldestPolicy:丢弃掉阻塞队列中存放时间最久的任务(队列头元素),并且为当前所提交的任务留出一个队列的空闲空间,以便将其放进到队列中(不一定能保证一定能成功放进去)

  4. CallerRunsPolicy: 直接由提交任务的线程来运行这个提交的任务,比如是main线程提交的任务,那么就由 main线程来执行

在线程池中,最好将偏向锁的标记关闭:因为线程池本身就有大量的线程执行任务,偏向锁反而会影响线程的执行速度

------------------------------------------蓝色的分割线----------------------------------------------------------
------------------------------------------蓝色的分割线----------------------------------------------------------

线程池执行任务的核心逻辑分析

(因为我们是有梦想的程序员,还是要更深入的理解线程池的执行逻辑)

对于线程池来说,其提供了 execute 与 submit 两种方式来向线程池提交任务,总体来说, submit 方法可以取代 execute 方法的, 因为它既可以接收 Callable 任务,也可以接收 Runnable 任务

  • 因为execute 方法接收的参数是 Runnable 类型的参数,而它的run 方法是没有返回值。
  • 但是 ExecutorService 线程池里面的 submit 方法重载了三个,分别对 Runnable 和 Callable 类型的参数都有,而 Callable 是有返回值的
    在这里插入图片描述

宏观上看线程池的执行策略

  • 如果线程池中正在执行的线程数 < corePoolSize ,那么线程池就会优先选择创建新的线程而非将提交的任务加到阻塞队列中
  • 如果线程池中正在执行的线程数 >= corePoolSize ,那么线程池就会优先选择对提交的任务进行阻塞排队而非创建新的线程
  • 如果提交的任务无法加入到阻塞队列中,那么线程池就会创建新的线程(当然创建的线程不会超过规定的最大的线程数)。如果创建的线程数 超过了 maxnumPoolSize ,那么拒绝策略就会起作用

关于线程池任务提交的总结:

  1. 两种提交方式: submit 和 execute

  2. submit 有三个重载的方式,无论是哪种方式,最终都是将传递进来的任务转换成一个 Callable 对象进行处理
    在这里插入图片描述
    在这里插入图片描述

  3. 当 Callable 对象构造完毕后,最终都会调用 Executor 接口中声明的 execute 方法进行统一的处理

在这里插入图片描述

线程池状态分析与源码解读

线程池一共有五种状态

  1. RUNNING: 线程池可以接收新的任务提交,并且还可以正常处理阻塞队列中的任务。
  2. SHUTDOWN: 不再接收新的任务提交,不过线程可以继续处理阻塞队列中的任务。
  3. STOP: 不再接收新的任务,同时还会丢弃阻塞队列中的既有任务。此外,它还会中断正在处理中的任务。
  4. TIDYING: 所有任务执行完毕后(同时也涵盖了阻塞队列中的任务),当前线程池中的活动的线程数量将为0,将会调用 terminated 方法
  5. TERMINATED: 线程池的终止状态,当 terminated 方法执行完毕后,线程池将会处于该状态之下。
    在这里插入图片描述

额外还提供了三个方法:

  1. int runStateOf(int c): 线程池运行的状态
  2. int workerCountOf(int c):计算线程池中运行的线程的数量
  3. int ctlOf(int rs, int wc):重新计算线程池原子整型变量 ctl 的值

线程池状态迁移

  • RUNNING ->> SHUTDOWN:当调用了线程池的 shutdown 方法之后,或者当 finalize 方法被隐式调用后(该方法内部会调用 shutdown 方法)
    在这里插入图片描述
    在这里插入图片描述

  • RUNNING,SHUTDOWN ->> STOP:当调用了线程池的 shutdownNow 方法时
    在这里插入图片描述

  • SHUTDOWN ->> TIDYING :在线程池与阻塞队列均变成空时

  • STOP ->> TIDYING : 在线程池变为空时

  • TIDYING ->> TERMINATED:在 terminated 方法被执行完毕时

------------------------------------------红色的分割线----------------------------------------------------------
------------------------------------------红色的分割线----------------------------------------------------------

第二个主角:ForkJoinPool

ForkJoinPool 是 jdk 1.7 才引入进来的。它也是间接的继承了 ExecutorService 这个接口。下面我们来讲讲它主要的功能是什么?它能帮我们解决什么问题…

**背景:**假设此时有A、B两个任务,A是小任务,执行时间很短,而B任务是大任务,执行时间稍微长一些。

对于普通的线程池来说,一个任务只能由一个线程去执行。这样导致的结果就是,一些线程很忙,一些线程就很闲。

这就是 ForkJoinPool 存在,解决这个问题的。对于ForkJoinPool,它可以将任务进行拆分,怎么拆分,拆分成的一个执行单元大小都是需要开发人员自己写逻辑去界定。

Fork:分叉、分流
Join:连接、合并

不会画脑图的Jarvis
ForkJoinPool 工作窃取:
是指两个线程A、B,对应两个任务队列,队列是双向的。A线程在把自己的任务队列里面的线程执行完毕之后,可以从双向队列的另一边 “窃取” B线程的队列里面的任务拿来执行。

ForkJoinPool怎么向其提交任务呢?

从ThreadPoolExecutor 我们得知,提交任务的方式无非是: Runnable、Callable。
但是对于ForkJoinPool来说,它自己还有独特的任务提交方式。而使用这种独有的任务提交方式,它的工作窃取算法才能生效。

ForkJoinTask这个抽象类是 ForkJoinPool 提交的任务方式。它的实现的子类也有很多…
但是最典型的,匹配 ForkJoinPool 使用的子类是: RecursiveAction、ForkJoinTask
在这里插入图片描述
这两个子类,也是分别都是抽象类,所以我们在使用的时候,都需要去实现它。

RecursiveAction、ForkJoinTask对比分析

  • 两个抽象类都有一个很重要的 compute 方法。区别在于 RecursiveAction 的compute 方法是没有返回值,就相当于我们向线程池提交的任务是没有返回值的任务,或者说不需要知道它返回值的任务。

  • 而 ForkJoinTask 的 compute 方法是有一个泛型的返回值。相当于我们的任务是需要一个返回结果的

在这里插入图片描述
在这里插入图片描述
具体事例

我们会创建一个任务,这个任务需要传递两个 int 类型的值,我们的任务会计算出两个数字之间的数字的总和。

(求和是需要一个结果的,所以我们的任务是基于 ForkJoinTask)

public class MyTest3 {


    public static void main(String[] args) {
        ForkJoinPool forkJoinPool = new ForkJoinPool(2);

        MyTask task = new MyTask(1, 100);

        Integer invoke = forkJoinPool.invoke(task);

        System.out.println(invoke);
        forkJoinPool.shutdownNow();

    }
}

class MyTask extends RecursiveTask<Integer> {

    //小任务之间的间隔数
    private int limit = 4;

    private int firstIndex;

    private int lastIndex;

    public MyTask(int firstIndex, int lastIndex) {
        this.firstIndex = firstIndex;
        this.lastIndex = lastIndex;
    }

    @Override
    protected Integer compute() {
        int result = 0;

        //两个数的间隔
        int gap = lastIndex - firstIndex;

        //判断间隔是否小于规定的原子任务间隔
        boolean flag = gap < limit;
        //计算逻辑

        if (flag) {
            System.out.println(Thread.currentThread().getName());
            for (int i = firstIndex; i <= lastIndex; i++) {
                result += i;
            }
        } else {
            //两个数大于间隔,就要拆分
            int middleIndex = (firstIndex + lastIndex) / 2;

            //这儿有种 递归 那味道了....
            MyTask leftTask = new MyTask(firstIndex, middleIndex);
            MyTask rightTask = new MyTask(middleIndex + 1, lastIndex);

            //这个方法,会重新再次调用 compute 方法
            invokeAll(leftTask, rightTask);
            
            // join 方法会等待子任务的结果,然后汇总....
            int leftTaskResult = leftTask.join();
            int rightTaskResult = rightTask.join();

            result = leftTaskResult + rightTaskResult;

        }
        return result;
    }
    
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值