java中常见的6种线程池示例详解

  • 之前我们介绍了线程池的四种拒绝策略,了解了线程池参数的含义,那么今天我们来聊聊Java 中常见的几种线程池,以及在jdk7 加入的 ForkJoin 新型线程池
  • 首先我们列出Java 中的六种线程池如下
线程池名称 描述
FixedThreadPool 核心线程数与最大线程数相同
SingleThreadExecutor 一个线程的线程池
CachedThreadPool 核心线程为0,最大线程数为Integer. MAX_VALUE
ScheduledThreadPool 指定核心线程数的定时线程池
SingleThreadScheduledExecutor 单例的定时线程池
ForkJoinPool JDK 7 新加入的一种线程池

在了解集中线程池时我们先来熟悉一下主要几个类的关系, ThreadPoolExecutor 的类图,以及 Executors 的主要方法:

上面看到的类图,方便帮助下面的理解和查看,我们可以看到一个核心类 ExecutorService , 这是我们线程池都实现的基类,我们接下来说的都是它的实现类。

FixedThreadPool

FixedThreadPool 线程池的特点是它的核心线程数和最大线程数一样,我们可以看它的实现代码在 Executors#newFixedThreadPool(int) 中,如下:

 public static ExecutorService newFixedThreadPool(int nThreads) {
 return new ThreadPoolExecutor(nThreads, nThreads,
  0L, TimeUnit.MILLISECONDS,
  new LinkedBlockingQueue<Runnable>());
 }

我们可以看到方法内创建线程调用的实际是 ThreadPoolExecutor 类,这是线程池的核心执行器,传入的 nThread 参数作为核心线程数和最大线程数传入,队列采用了一个链表结构的有界队列。

  • 这种线程池我们可以看作是固定线程数的线程池,它只有在开始初始化的时候线程数会从0开始创建,但是创建好后就不再销毁,而是全部作为常驻线程池,这里如果对线程池参数不理解的可以看之前文章 《解释线程池各个参数的含义》。
  • 对于这种线程池他的第三个和第四个参数是没意义,它们是空闲线程存活时间,这里都是常驻不存在销毁,当线程处理不了时会加入到阻塞队列,这是一个链表结构的有界阻塞队列,最大长度是Integer. MAX_VALUE

SingleThreadExecutor

SingleThreadExecutor 线程的特点是它的核心线程数和最大线程数均为1,我们也可以将其任务是一个单例线程池,它的实现代码是 Executors#newSingleThreadExcutor() , 如下:

 public static ExecutorService newSingleThreadExecutor() {
 return new FinalizableDelegatedExecutorService
 (new ThreadPoolExecutor(1, 1,
   0L, TimeUnit.MILLISECONDS,
   new LinkedBlockingQueue<Runnable>()));
 }

 public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
 return new FinalizableDelegatedExecutorService
 (new ThreadPoolExecutor(1, 1,
   0L, TimeUnit.MILLISECONDS,
   new LinkedBlockingQueue<Runnable>(),
   threadFactory));
 }
  • 上述代码中我们发现它有一个重载函数,传入了一个ThreadFactory 的参数,一般在我们开发中会传入我们自定义的线程创建工厂,如果不传入则会调用默认的线程工厂
  • 我们可以看到它与 FixedThreadPool 线程池的区别仅仅是核心线程数和最大线程数改为了1,也就是说不管任务多少,它只会有唯一的一个线程去执行
  • 如果在执行过程中发生异常等导致线程销毁,线程池也会重新创建一个线程来执行后续的任务
  • 这种线程池非常适合所有任务都需要按被提交的顺序来执行的场景,是个单线程的串行。

CachedThreadPool

cachedThreadPool 线程池的特点是它的常驻核心线程数为0,正如其名字一样,它所有的县城都是临时的创建,关于它的实现在 Executors#newCachedThreadPool() 中,代码如下:

 public static ExecutorService newCachedThreadPool() {
 return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
   60L, TimeUnit.SECONDS,
   new SynchronousQueue<Runnable>());
 }

 public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
 return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
   60L, TimeUnit.SECONDS,
   new SynchronousQueue<Runnable>(),
   threadFactory);
 }
  • 从上述代码中我们可以看到 CachedThreadPool 线程池中,最大线程数为 Integer.MAX_VALUE , 意味着他的线程数几乎可以无限增加。
  • 因为创建的线程都是临时线程,所以他们都会被销毁,这里空闲 线程销毁时间是60秒,也就是说当线程在60秒内没有任务执行则销毁
  • 这里我们需要注意点,它使用了 SynchronousQueue 的一个阻塞队列来存储任务,这个队列是无法存储的,因为他的容量为0,它只负责对任务的传递和中转,效率会更高,因为核心线程都为0,这个队列如果存储任务不存在意义。

ScheduledThreadPool

ScheduledThreadPool 线程池是支持定时或者周期性执行任务,他的创建代码 Executors.newSchedsuledThreadPool(int) 中,如下所示:

 public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
 return new ScheduledThreadPoolExecutor(corePoolSize);
 }

 public static ScheduledExecutorService newScheduledThreadPool(
 int corePoolSize, ThreadFactory threadFactory) {
 return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
 }

我们发现这里调用了 ScheduledThreadPoolExecutor 这个类的构造函数,进一步查看发现 ScheduledThreadPoolExecutor 类是一个继承了 ThreadPoolExecutor 的,同时实现了 ScheduledExecutorService 接口,我们看到它的几个构造函数都是调用父类 ThreadPoolExecutor 的构造函数

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

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

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

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

从上面代码我们可以看到和其他线程池创建并没有差异,只是这里的任务队列是 DelayedWorkQueue 关于阻塞丢列我们下篇文章专门说,这里我们先创建一个周期性的线程池来看一下

 public static void main(String[] args) {
 ScheduledExecutorService service = Executors.newScheduledThreadPool(5);
 // 1. 延迟一定时间执行一次
 service.schedule(() ->{
 System.out.println("schedule ==> 云栖简码-i-code.online");
 },2, TimeUnit.SECONDS);

 // 2. 按照固定频率周期执行
 service.scheduleAtFixedRate(() ->{
 System.out.println("scheduleAtFixedRate ==> 云栖简码-i-code.online");
 },2,3,TimeUnit.SECONDS);

 //3. 按照固定频率周期执行
 service.scheduleWithFixedDelay(() -> {
 System.out.println("scheduleWithFixedDelay ==> 云栖简码-i-code.online");
 },2,5,TimeUnit.SECONDS);

 }

上面代码是我们简单创建了 newScheduledThreadPool ,同时演示了里面的三个核心方法,首先看执行的结果:

首先我们看第一个方法 schedule , 它有三个参数,第一个参数是线程任务,第二个 delay 表示任务执行延迟时长,第三个 unit 表示延迟时间的单位,如上面代码所示就是延迟两秒后执行任务

 public ScheduledFuture<?> schedule(Runnable command,
   long delay, TimeUnit unit);

第二个方法是 scheduleAtFixedRate 如下, 它有四个参数, command 参数表示执行的线程任务 , initialDelay 参数表示第一次执行的延迟时间, period 参数表示第一次执行之后按照多久一次的频率来执行,最后一个参数是时间单位。如上面案例代码所示,表示两秒后执行第一次,之后按每隔三秒执行一次

 public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
    long initialDelay,
    long period,
    TimeUnit unit);

第三个方法是 scheduleWithFixedDelay 如下,它与上面方法是非常类似的,也是周期性定时执行, 参数含义和上面方法一致。这个方法和 scheduleAtFixedRate 的区别主要在于时间的起点计时不同

 public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
    long initialDelay,
    long delay,
    TimeUnit unit);

scheduleAtFixedRate 是以任务开始的时间为时间起点来计时,时间到就执行第二次任务,与任务执行所花费的时间无关;而 scheduleWithFixedDelay 是以任务执行结束的时间点作为计时的开始。如下所示

SingleThreadScheduledExecutor 它实际和 ScheduledThreadPool 线程池非常相似,它只是 ScheduledThreadPool 的一个特例,内部只有一个线程,它只是将 ScheduledThreadPool 的核心线程数设置为了 1。如源码所示:

 public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
 return new DelegatedScheduledExecutorService
 (new ScheduledThreadPoolExecutor(1));
 }

上面我们介绍了五种常见的线程池,对于这些线程池我们可以从核心线程数、最大线程数、存活时间三个维度进行一个简单的对比,有利于我们加深对这几种线程池的记忆。

FixedThreadPool SingleThreadExecutor CachedThreadPool ScheduledThreadPool SingleThreadScheduledExecutor
corePoolSize 构造函数传入 1 0 构造函数传入 1
maxPoolSize 同corePoolSize 1 Integer. MAX_VALUE Integer. MAX_VALUE Integer. MAX_VALUE
keepAliveTime 0 0 60 0 0

ForkJoinPool ForkJoinPool 这是一个在 JDK7 引入的新新线程池,它的主要特点是可以充分利用多核 CPU , 可以把一个任务拆分为多个子任务,这些子任务放在不同的处理器上并行执行,当这些子任务执行结束后再把这些结果合并起来,这是一种分治思想。 ForkJoinPool 也正如它的名字一样,第一步进行 Fork 拆分,第二步进行 Join 合并,我们先来看一下它的类图结构

ForkJoinPool 的使用也是通过调用 submit(ForkJoinTask<T> task) 或 invoke(ForkJoinTask<T> task) 方法来执行指定任务了。其中任务的类型是 ForkJoinTask 类,它代表的是一个可以合并的子任务,他本身是一个抽象类,同时还有两个常用的抽象子类 RecursiveAction 和 RecursiveTask ,其中 RecursiveTask 表示的是有返回值类型的任务,而 RecursiveAction 则表示无返回值的任务。下面是它们的类图:

下面我们通过一个简单的代码先来看一下如何使用 ForkJoinPool 线程池

/**
 * @url: i-code.online
 * @author: AnonyStar
 * @time: 2020/11/2 10:01
 */
public class ForkJoinApp1 {

 /**
 目标: 打印0-200以内的数字,进行分段每个间隔为10以上,测试forkjoin
 */
 public static void main(String[] args) {
 // 创建线程池,
 ForkJoinPool joinPool = new ForkJoinPool();
 // 创建根任务
 SubTask subTask = new SubTask(0,200);
 // 提交任务
 joinPool.submit(subTask);
 //让线程阻塞等待所有任务完成 在进行关闭
 try {
 joinPool.awaitTermination(2, TimeUnit.SECONDS);
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 joinPool.shutdown();
 }
}

class SubTask extends RecursiveAction {

 int startNum;
 int endNum;

 public SubTask(int startNum,int endNum){
 super();
 this.startNum = startNum;
 this.endNum = endNum;
 }

 @Override
 protected void compute() {

 if (endNum - startNum < 10){
 // 如果分裂的两者差值小于10 则不再继续,直接打印
 System.out.println(Thread.currentThread().getName()+": [startNum:"+startNum+",endNum:"+endNum+"]");
 }else {
 // 取中间值
 int middle = (startNum + endNum) / 2;
 //创建两个子任务,以递归思想,
 SubTask subTask = new SubTask(startNum,middle);
 SubTask subTask1 = new SubTask(middle,endNum);
 //执行任务, fork() 表示异步的开始执行
 subTask.fork();
 subTask1.fork();
 }
 }
}

结果:

从上面的案例我们可以看到我们,创建了很多个线程执行,因为我测试的电脑是12线程的,所以这里实际是创建了12个线程,也侧面说明了充分调用了每个处理的线程处理能力 上面案例其实我们发现很熟悉的味道,那就是以前接触过的递归思想,将上面的案例图像化如下,更直观的看到,

上面的例子是无返回值的案例,下面我们来看一个典型的有返回值的案例,相信大家都听过及很熟悉斐波那契数列,这个数列有个特点就是最后一项的结果等于前两项的和,如: 0,1,1,2,3,5...f(n-2)+f(n-1) , 即第0项为0 ,第一项为1,则第二项为 0+1=1 ,以此类推。我们最初的解决方法就是使用递归来解决,如下计算第n项的数值:

 private int num(int num){
 if (num <= 1){
 return num;
 }
 num = num(num-1) + num(num -2);
 return num;
 }

从上面简单代码中可以看到,当 n<=1 时返回 n , 如果 n>1 则计算前一项的值 f1 ,在计算前两项的值 f2 , 再将两者相加得到结果,这就是典型的递归问题,也是对应我们的 ForkJoin 的工作模式,如下所示,根节点产生子任务,子任务再次衍生出子子任务,到最后在进行整合汇聚,得到结果。

我们通过 ForkJoinPool 来实现斐波那契数列的计算,如下展示:

/**
 * @url: i-code.online
 * @author: AnonyStar
 * @time: 2020/11/2 10:01
 */
public class ForkJoinApp3 {

 public static void main(String[] args) throws ExecutionException, InterruptedException {
 ForkJoinPool pool = new ForkJoinPool();
 //计算第二是项的数值
 final ForkJoinTask<Integer> submit = pool.submit(new Fibonacci(20));
 // 获取结果,这里获取的就是异步任务的最终结果
 System.out.println(submit.get());

 }
}

class Fibonacci extends RecursiveTask<Integer>{

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

 @Override
 protected Integer compute() {
 if (num <= 1) return num;
 //创建子任务
 Fibonacci subTask1 = new Fibonacci(num - 1);
 Fibonacci subTask2 = new Fibonacci(num - 2);
 // 执行子任务
 subTask1.fork();
 subTask2.fork();
 //获取前两项的结果来计算和
 return subTask1.join()+subTask2.join();
 }
}

通过 ForkJoinPool 可以极大的发挥多核处理器的优势,尤其非常适合用于递归的场景,例如树的遍历、最优路径搜索等场景。 上面说的是 ForkJoinPool 的使用上的,下面我们来说一下其内部的构造,对于我们前面说的几种线程池来说,它们都是里面只有一个队列,所有的线程共享一个。但是在 ForkJoinPool 中,其内部有一个共享的任务队列,除此之外每个线程都有一个对应的双端队列 Deque , 当一个线程中任务被 Fork 分裂了,那么分裂出来的子任务就会放入到对应的线程自己的 Deque 中,而不是放入公共队列。这样对于每个线程来说成本会降低很多,可以直接从自己线程的队列中获取任务而不需要去公共队列中争夺,有效的减少了线程间的资源竞争和切换。

有一种情况,当线程有多个如 t1,t2,t3... ,在某一段时间线程 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 ” 算法和双端队列很好地平衡了各线程的负载。

总结

到此这篇关于java中常见的6种线程池示例详解的文章就介绍到这了,更多相关java常见线程池内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Java中四种线程池的使用示例详解

    在什么情况下使用线程池? 1.单个任务处理的时间比较短 2.将需处理的任务的数量大 使用线程池的好处: 1.减少在创建和销毁线程上所花的时间以及系统资源的开销 2.如不使用线程池,有可能造成系统创建大量线程而导致消耗完系统内存以及"过度切换". 本文详细的给大家介绍了关于Java中四种线程池的使用,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍: FixedThreadPool 由Executors的newFixedThreadPool方法创建.它是一种线程数量固定的线程

  • java中通用的线程池实例代码

    复制代码 代码如下: package com.smart.frame.task.autoTask; import java.util.Collection;import java.util.Vector; /** * 任务分发器 */public class TaskManage extends Thread{    protected Vector<Runnable> tasks = new Vector<Runnable>();    protected boolean run

  • 深入java线程池的使用详解

    在Java 5.0之前启动一个任务是通过调用Thread类的start()方法来实现的,任务的提于交和执行是同时进行的,如果你想对任务的执行进行调度或是控制 同时执行的线程数量就需要额外编写代码来完成.5.0里提供了一个新的任务执行架构使你可以轻松地调度和控制任务的执行,并且可以建立一个类似数据库连接 池的线程池来执行任务.这个架构主要有三个接口和其相应的具体类组成.这三个接口是Executor, ExecutorService.ScheduledExecutorService,让我们先用一个图

  • Java 线程池ExecutorService详解及实例代码

    Java 线程池ExecutorService 1.线程池 1.1什么情况下使用线程池 单个任务处理的时间比较短. 将需处理的任务的数量大. 1.2使用线程池的好处 减少在创建和销毁线程上所花的时间以及系统资源的开销. 如果不使用线程池,有可能造成系统创建大量线程而导致消耗系统内存以及"过度切换"; 2.ExecutorService和Executors 2.1简介 ExecutorService是一个接口,继承了Executor, public interface ExecutorS

  • java线程池:获取运行线程数并控制线程启动速度的方法

    在java里, 我们可以使用Executors.newFixedThreadPool 来创建线程池, 然后就可以不停的创建新任务,并用线程池来执行了. 在提交任务时,如果线程池已经被占满,任务会进到一个队列里等待执行. 这种机制在一些特定情况下会有些问题.今天我就遇到一种情况:创建线程比线程执行的速度要快的多,而且单个线程占用的内存又多,所以很快内存就爆了. 想了一个办法,就是在提交任务之前,先检查目前正在执行的线程数目,只有没把线程池占满的时候在去提交任务. 代码很简单: int thread

  • Java实现终止线程池中正在运行的定时任务

    最近项目中遇到了一个新的需求,就是实现一个可以动态添加定时任务的功能.说到这里,有人可能会说简单啊,使用quartz就好了,简单粗暴.然而quartz框架太重了,小项目根本不好操作啊.当然,也有人会说,jdk提供了timer的接口啊,完全够用啊.但是我们项目的需求完全是多线程的模型啊,而timer是单线程的,so,楼主最后还是选择了jdk的线程池. 线程池是什么 Java通过Executors提供四种线程池,分别为: newCachedThreadPool :创建一个可缓存线程池,如果线程池长度

  • java简单实现多线程及线程池实例详解

    本文为大家分享了java多线程的简单实现及线程池实例,供大家参考,具体内容如下 一.多线程的两种实现方式 1.继承Thread类的多线程 /** * 继承Thread类的多线程简单实现 */ public class extThread extends Thread { public void run(){ for(int i=0;i<100;i++){ System.out.println(getName()+"-"+i); } } public static void mai

  • java线程池使用后到底要关闭吗

    线程池做什么 网络请求通常有两种形式: 第一种,请求不是很频繁,而且每次连接后会保持相当一段时间来读数据或者写数据,最后断开,如文件下载,网络流媒体等. 另一种形式是请求频繁,但是连接上以后读/写很少量的数据就断开连接.考虑到服务的并发问题,如果每个请求来到以后服务都为它启动一个线程,那么这对服务的资源可能会造成很大的浪费,特别是第二种情况. 因为通常情况下,创建线程是需要一定的耗时的,设这个时间为T1,而连接后读/写服务的时间为T2,当T1>>T2时,我们就应当考虑一种策略或者机制来控制,使

  • Java 线程池详解及实例代码

    线程池的技术背景 在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源.在Java中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收. 所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁.如何利用已有对象来服务就是一个需要解决的关键问题,其实这就是一些"池化资源"技术产生的原因. 例如Android中常见到的很多通用组件一般都离不开"池"的概念,如各种图片

  • 详谈Java几种线程池类型介绍及使用方法

    一.线程池使用场景 •单个任务处理时间短 •将需处理的任务数量大 二.使用Java线程池好处 1.使用new Thread()创建线程的弊端: •每次通过new Thread()创建对象性能不佳. •线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或oom. •缺乏更多功能,如定时执行.定期执行.线程中断. 2.使用Java线程池的好处: •重用存在的线程,减少对象创建.消亡的开销,提升性能. •可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞

  • 四种Java线程池用法解析

    本文为大家分析四种Java线程池用法,供大家参考,具体内容如下 1.new Thread的弊端 执行一个异步任务你还只是如下new Thread吗? new Thread(new Runnable() { @Override public void run() { // TODO Auto-generated method stub } } ).start(); 那你就out太多了,new Thread的弊端如下: a. 每次new Thread新建对象性能差. b. 线程缺乏统一管理,可能无限

随机推荐