温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

常见的线程池有哪些

发布时间:2021-10-25 09:22:29 来源:亿速云 阅读:163 作者:iii 栏目:编程语言

这篇文章主要介绍“常见的线程池有哪些”,在日常操作中,相信很多人在常见的线程池有哪些问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”常见的线程池有哪些”的疑惑有所帮助!接下来,请跟着小编一起来学习吧!

一:几种常见线程池

1.1 FixedThreadPool

FixedThreadPool线程池,它的核心线程数和最大线程数是一样的,所以可以把它看作是固定线程数的线程池。

特点是:线程池中的线程数除了初始阶段需要从 0 开始增加外,之后的线程数量就是固定的,就算任务数超过线程数,线程池也不会再创建更多的线程来处理任务,而是会把超出线程处理能力的任务放到任务队列中进行等待。而且就算任务队列满了,到了本该继续增加线程数的时候,由于它的最大线程数和核心线程数是一样的,所以也无法再增加新的线程了。

常见的线程池有哪些

如图所示,线程池有 t0~t9,10 个线程,它们会不停地执行任务,如果某个线程任务执行完了,就会从任务队列中获取新的任务继续执行,期间线程数量不会增加也不会减少,始终保持在 10 个。

1.2 CachedThreadPool

CachedThreadPool,可以称作可缓存线程池,它的特点在于线程数是几乎可以无限增加的(实际最大可以达到 Integer.MAX_VALUE,为 2^31-1),而当线程闲置时还可以对线程进行回收。

CachedThreadPool 线程池它也有一个用于存储提交任务的队列,但这个队列是 SynchronousQueue,队列的容量为0,实际不存储任何任务,它只负责对任务进行中转和传递,所以效率比较高

当提交一个任务后,线程池会判断已创建的线程中是否有空闲线程,如果有空闲线程则将任务直接指派给空闲线程,如果没有空闲线程,则新建线程去执行任务,这样就做到了动态地新增线程。如下方代码所示。

/**
 * @date 2020/11/15 23:17
 *
 * @discription 缓存线程池
 */
public class CachedThreadPool {
    
    public static void main(String[] args) {

        ExecutorService service = Executors.newCachedThreadPool();
        for (int i = 0; i < 1000; i++) {
            service.execute(new ThreadPoolDemo.Task());
        }
    }
    static class Task implements Runnable{
        @Override
        public void run() {
            System.out.println("Thread Name: " + Thread.currentThread().getName());
        }
    }
}

因为执行任务简单,所以在 for 循环提交任务结束前,就可能导致先执行的任务被执行完了,线程空闲出来执行后面的任务,从而不会出现线程名称为pool-1-thread-1000的线程。

假设这些任务处理的时间非常长,因为 for 循环提交任务的操作是非常快的,但执行任务却比较耗时,就可能导致 1000 个任务都提交完了但第一个任务还没有被执行完,所以此时 CachedThreadPool 就可以动态的伸缩线程数量,随着任务的提交,不停地创建 1000 个线程来执行任务,而当任务执行完之后,假设没有新的任务了,那么大量的闲置线程又会造成内存资源的浪费,这时线程池就会检测线程在 60 秒内有没有可执行任务,如果没有就会被销毁,最终线程数量会减为 0。

1.3 ScheduledThreadPool

线程池 ScheduledThreadPool,它支持定时或周期性执行任务。

比如每隔 10 秒钟执行一次任务,而实现这种功能的方法主要有 3 种,如代码所示:

/**
 * @date 2020/11/15 23:34
 *
 * @discription 支持定时或周期性执行任务的线程池
 */
public class ScheduledThreadPool {

    public static void main(String[] args) {
        ScheduledExecutorService service = Executors.newScheduledThreadPool(10);

        /**
         * schedule —— 比较简单表示延迟指定时间后执行一次任务,
         * 如果代码中设置参数为 10 秒,也就是 10 秒后执行一次任务后就结束。
         */
        service.schedule(new ThreadPoolDemo.Task(),10, TimeUnit.SECONDS);

        /**
         * scheduleAtFixedRate —— 表示以固定的频率执行任务,
         * 它的第二个参数 initialDelay 表示第一次延时时间,
         * 第三个参数 period 表示周期也就是第一次延时后每次延时多长时间执行一次任务。
         */
        service.scheduleAtFixedRate(new ThreadPoolDemo.Task(), 10, 10, TimeUnit.SECONDS);

        /**
         * scheduleWithFixedDelay —— 与第二种方法类似,也是周期执行任务,区别在于对周期的定义,
         * 之前的 scheduleAtFixedRate 是以任务开始的时间为时间起点,
		 开始计时时间到就开始执行第二次任务,而不管任务需要花多久执行;
         * 而 scheduleWithFixedDelay 方法以任务结束的时间为下一次循环的时间起点开始计时。
         */
        service.scheduleWithFixedDelay(new ThreadPoolDemo.Task(), 10, 10, TimeUnit.SECONDS);
    }
}

比如:假设某个同学正在熬夜写代码,需要喝咖啡来提神,假设每次喝咖啡都需要花10分钟的时间,如果此时采用第2种方法 scheduleAtFixedRate,时间间隔设置为 1 小时,那么他将会在每个整点喝一杯咖啡,以下是时间表:

  • 00:00: 开始喝咖啡

  • 00:10: 喝完了

  • 01:00: 开始喝咖啡

  • 01:10: 喝完了

  • 02:00: 开始喝咖啡

  • 02:10: 喝完了

采用第3种方法 scheduleWithFixedDelay,时间间隔同样设置为 1 小时,那么由于每次喝咖啡需要10分钟,而 scheduleWithFixedDelay 是以任务完成的时间为时间起点开始计时的,所以第2次喝咖啡的时间将会在1:10,而不是1:00整,以下是时间表:

  • 00:00: 开始喝咖啡

  • 00:10: 喝完了

  • 01:10: 开始喝咖啡

  • 01:20: 喝完了

  • 02:20: 开始喝咖啡

  • 02:30: 喝完了

1.4 SingleThreadExecutor

SingleThreadExecutor,它会使用唯一的线程去执行任务,原理和 FixedThreadPool 是一样的,只不过这里线程只有一个,如果线程在执行任务的过程中发生异常,线程池也会重新创建一个线程来执行后续的任务。

这种线程池由于只有一个线程,所以非常适合用于所有任务都需要按被提交的顺序依次执行的场景,而前几种线程池不一定能够保障任务的执行顺序等于被提交的顺序,因为它们是多线程并行执行的。

1.5 SingleThreadScheduledExecutor

SingleThreadScheduledExecutor,它实际和第三种 ScheduledThreadPool 线程池非常相似,它只是 ScheduledThreadPool 的一个特例,内部只有一个线程。

注意:它只是将 ScheduledThreadPool 的核心线程数设置为了 1,即new ScheduledThreadPoolExecutor(1),并非最大线程为一的意思。

1.6 总结

常见的线程池有哪些

二:JDK1.7 新增新增线程池 ForkJoinPool

ForkJoinPool,这个线程池是在 JDK 7 加入的,主要用法和之前的线程池是相同的,也是把任务交给线程池去执行,线程池中也有任务队列来存放任务。ForkJoinPool 非常适合用于递归的场景,例如树的遍历、最优路径搜索等场景。

ForkJoinPool 线程池和之前的线程池有两点非常大的不同之处。

第一点它非常适合执行可以产生子任务的任务

比如:我们有一个 Task,这个 Task 可以产生三个子任务,三个子任务并行执行完毕后将结果汇总给 Result,比如说主任务需要执行非常繁重的计算任务,我们就可以把计算拆分成三个部分,这三个部分是互不影响相互独立的,这样就可以利用 CPU 的多核优势,并行计算,然后将结果进行汇总。这里面主要涉及两个步骤,第一步是拆分也就是 Fork,第二步是汇总也就是 Join,这也是ForkJoinPool 线程池名字的由来。

ForkJoinPool 线程池有多种方法可以实现任务的分裂和汇总,其中一种用法计算斐波那契数列如下方代码所示。

/**
 * @date 2020/11/16 0:06
 *
 * @discription ForkJoinPool计算斐波那契数列
 *              打印出斐波那契数列的第 0 到 9 项的值:
 */
public class Fibonacci extends RecursiveTask<Integer> {

    int n;

    public Fibonacci(int n) {
        this.n = n;
    }

    @Override
    protected Integer compute() {
        if(n <= 1){
            return n;
        }
        Fibonacci f1 = new Fibonacci(n-1);
        f1.fork();
        Fibonacci f2 = new Fibonacci(n-2);
        f2.fork();

        return f1.join() + f2.join();
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {

        ForkJoinPool forkJoinPool = new ForkJoinPool();
        for (int i = 0; i < 10; i++) {
            ForkJoinTask task = forkJoinPool.submit(new Fibonacci(i));
            System.out.println(task.get());
        }
    }
}

它首先继承了 RecursiveTask,RecursiveTask 类是对ForkJoinTask 的一个简单的包装,这时我们重写 compute() 方法,当 n<=1 时直接返回,当 n>1 就创建递归任务,也就是 f1 和 f2,然后我们用 fork() 方法分裂任务并分别执行,最后在 return 的时候,使用 join() 方法把结果汇总,这样就实现了任务的分裂和汇总。

第二点不同之处在于内部结构

之前的线程池所有的线程共用一个队列,但 ForkJoinPool 线程池中每个线程都有自己独立的任务队列,如图所示。

常见的线程池有哪些

可以看到 ForkJoinPool 线程池和其他线程池很多地方都是一样的,但重点区别在于除了有一个共用的任务队列之外,它每个线程都有一个自己的双端队列来存储分裂出来的子任务。

  • 当线程中的任务被 Fork 分裂了,分裂出来的子任务放入线程自己的 deque 里,而不是放入公共的任务队列中。对于当前线程来说以直接在自己的任务队列中获取,而不必去公共队列中争抢,也不会发生阻塞(除了 steal 情况外),减少了线程间的竞争和切换,是非常高效的。

  • 若此时线程有多个,而线程 t1 的任务特别繁重,分裂了数十个子任务,但是 t0 此时却无事可做,它自己的 deque 队列为空,这时为了提高效率,t0 就会想办法帮助 t1 执行任务,这就是“work-stealing”的含义。双端队列 deque 中,线程 t1 获取任务的逻辑是后进先出,而线程 t0 在“steal”偷线程 t1 的 deque 中的任务的逻辑是先进先出使用 “work-stealing” 算法和双端队列很好地平衡了各线程的负载

到此,关于“常见的线程池有哪些”的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注亿速云网站,小编会继续努力为大家带来更多实用的文章!

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI