六种常见的线程池

 1、FixedThreadPool

这个线程池的核心线程数和最大线程数是一样的,所以可以看作是固定线程数的线程池

特点是当线程达到核心线程数后,如果任务队列满了,也不会创建额外的非核心线程去执行任务,而是执行拒绝策略.

2、CachedThreadPool

这个线程池叫做缓存线程池,

特点线程数几乎是可以无限增加的(最大值是Integer.MAX_VALUE,基本不会达到),当线程闲置时还可以进行回收,而且它采用的存储任务的队列是SynchronousQueue队列,队列容量是0,实际不存储任务,只负责对任务的中转和传递,所以来一个任务线程池就看是否有空闲的线程,有的话就用空闲的线程去执行任务,否则就创建一个线程去执行,效率比较高.

3、ScheduledThreadPool

通过这个线程池的名字可以看出,它支持定时或者周期性执行任务,实现这种功能的方法主要有三种:

ScheduledExecutorService service = Executors.newScheduledThreadPool(10);
service.schedule(new Task(), 10, TimeUnit.SECONDS);
service.scheduleAtFixedRate(new Task(), 10, 10, TimeUnit.SECONDS);
service.scheduleWithFixedDelay(new Task(), 10, 10, TimeUnit.SECONDS);
  •  第一种是schedule,通过延迟指定时间后执行一次任务,代码中设置的是10秒,所以10秒后执行一次任务就结束.
  • 第二种是scheduleAtFixedRate,通过名称我们可以看出,第二种是以固定频率去执行任务,它的第二个参数initialDelay表示第一次延迟时间,第三个参数period表示周期,总体按照上面的代码意思就是,第一次延迟10秒后执行任务,然后,每次延迟10秒执行一次任务.
  • 第三种方法是scheduleWithFixeddelay这种与第二种方法类似,也是周期执行任务,不同的是对周期的定义,之前的scheduleAtFixedRate是以任务的开始时间为起点开始计时,时间到了就开始执行第二次任务,而不管任务需要多久执行,而scheduleWithFixeddelay是以任务结束的时间作为下一次循环开始的时间起点.

4、SingleThreadExecutor

第四种线程池中只有一个线程去执行任务,如果执行任务过程中发生了异常,则线程池会创建一个新线程来执行后续任务,这个线程因为只有一个线程,所以可以保证任务执行的有序性.

5、SingleThreadScheduleExecutor

这个线程池它和ScheduledThreadPool很相似,只不过它的内部也只有一个线程,他只是将核心线程数设置为了1,如果执行期间发生异常,同样会创建一个新线程去执行任务.

6、ForkJoinPool

最后一种线程池是ForkJoinPool,这个线程池是来支持将一个任务拆分成多个“小任务”并行计算,这个线程池是在jdk1.7之后加入的,它主要用于实现“分而治之”的算法,特别是分治之后递归调用的函数,这里只是对ForkJoinPool做了一个简单的介绍,我们先来介绍一下ForkJoinPool和之前的线程池主要的两个特点。

 

第一点是fork和join:

我们现来看看fork和join的含义,fork就是将任务分解成多个子任务,多个子任务互相独立,不受影响,执行的时候可以利用 CPU 的多核优势,并行计算,计算完成后各个子任务在调用join方法进行结果汇总,第一步是拆分也就是 Fork,第二步是汇总也就是 Join,我们通过下图来理解:

 

我们通过举例斐波那契数列来展示这个线程池的使用。

1.首先我们创建任务类FibonacciTask继承RecursiveTask类,重写compute方法。其中的ForkJoinTask代表一个可以并行、合并的任务,ForkJoinTask是一个抽象类,它还有两个抽象子类:RecusiveAction和RecusiveTask。其中RecusiveTask代表有返回值的任务,而RecusiveAction代表没有返回值的任务,

2.我们在compute方法中实现斐波那契数列计算并获取返回值。

3.在main方法中创建ForkJoinPool,并调用线程池的submit(ForkJoinTask<T>task)方法,通过获取返回的task.get()方法获取计算的返回值。

任务类:FibonacciTask

/**
 * 这里我们的定义任务类继承RecursiveTask,需要重写一个compute方法,或者任务执行的返回值
 * RecursiveAction和RecursiveTask是ForkJoinTask的两个抽象子类,
 * 其中的ForkJoinTask,代表一个可以并行、合并的任务其中RecursiveAction
 * 表示没有返回值的任务,RecursiveTask是有返回值的任务
 */
public class FibonacciTask extends RecursiveTask<Integer> {
    private int i;
    FibonacciTask(int i){
        this.i=i;
    }
    @Override
        protected Integer compute() {
        if(i<=1){
            return i;
        }
        FibonacciTask f1=new FibonacciTask(i-1);
        //用 fork() 方法分裂任务并分别执行
        f1.fork();
        FibonacciTask f2=new FibonacciTask(i-2);
        f2.fork();
        //使用 join() 方法把结果汇总
        return f1.join()+f2.join();
    }
}

 main方法:

public static void main(String[] args) {
        ForkJoinPool forkJoinPool=new ForkJoinPool();
        for(int i=0;i<10;i++){
            ForkJoinTask<Integer> task = forkJoinPool.submit(new FibonacciTask(i));
            try {
                System.out.println(task.get());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }
    }

 计算结果如下:

 

第二点是内部结构不同:

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

ForkJoinPool 线程池内部除了有一个共用的任务队列之外,每个线程还有一个对应的双端队列 deque,这时一旦线程中的任务被 Fork 分裂了,分裂出来的子任务放入线程自己的 deque 里,而不是放入公共的任务队列中(公共任务队列采用数组存放),如果此时有三个子任务放入线程 t1 的 deque 队列中,对于线程 t1 而言获取任务的成本就降低了,可以直接在自己的任务队列中获取而不必去公共队列中争抢也不会发生阻塞(除了后面会讲到的 steal 情况外),减少了线程间的竞争和切换,是非常高效的。

我们再考虑一种情况,此时线程有多个,而线程 t1 的任务特别繁重,分裂了数十个子任务,但是 t0 此时却无事可做,它自己的 deque 队列为空,这时为了提高效率,t0 就会想办法帮助 t1 执行任务,这就是“work-stealing”的含义。

双端队列 deque 中,线程 t1 获取任务的逻辑是后进先出,也就是LIFO(Last In Frist Out),而线程 t0 在“steal”偷线程 t1 的 deque 中的任务的逻辑是先进先出,也就是FIFO(Fast In Frist Out),如图所示,图中很好的描述了两个线程使用双端队列分别获取任务的情景。你可以看到,使用 “work-stealing” 算法和双端队列很好地平衡了各线程的负载。

 最后,我们用一张全景图来描述 ForkJoinPool 线程池的内部结构,你可以看到 ForkJoinPool 线程池和其他线程池很多地方都是一样的,但重点区别在于它每个线程都有一个自己的双端队列来存储分裂出来的子任务。ForkJoinPool 非常适合用于递归的场景,例如树的遍历、最优路径搜索等场景。

posted @ 2022-11-24 09:40  weslie  阅读(2638)  评论(0编辑  收藏  举报