之前我们介绍了线程池的四种拒绝策略,了解了线程池参数的含义,那么今天我们来聊聊Java 中常见的几种线程池,以及在jdk7 加入的 ForkJoin 新型线程池

首先我们列出Java 中的六种线程池如下线程池名称描述FixedThreadPool核心线程数与最大线程数相同

SingleThreadExecutor一个线程的线程池

CachedThreadPool核心线程为0,最大线程数为Integer. MAX_VALUE

ScheduledThreadPool指定核心线程数的定时线程池

SingleThreadScheduledExecutor单例的定时线程池

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

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

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

return new ThreadPoolExecutor(nThreads, nThreads,

0L, TimeUnit.MILLISECONDS,

new LinkedBlockingQueue());

}我们可以看到方法内创建线程调用的实际是 ThreadPoolExecutor 类,这是线程池的核心执行器,传入的 nThread 参数作为核心线程数和最大线程数传入,队列采用了一个链表结构的有界队列。这种线程池我们可以看作是固定线程数的线程池,它只有在开始初始化的时候线程数会从0开始创建,但是创建好后就不再销毁,而是全部作为常驻线程池,这里如果对线程池参数不理解的可以看之前文章 《解释线程池各个参数的含义》。

对于这种线程池他的第三个和第四个参数是没意义,它们是空闲线程存活时间,这里都是常驻不存在销毁,当线程处理不了时会加入到阻塞队列,这是一个链表结构的有界阻塞队列,最大长度是Integer. MAX_VALUE

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

return new FinalizableDelegatedExecutorService

(new ThreadPoolExecutor(1, 1,

0L, TimeUnit.MILLISECONDS,

new LinkedBlockingQueue()));

}

public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {

return new FinalizableDelegatedExecutorService

(new ThreadPoolExecutor(1, 1,

0L, TimeUnit.MILLISECONDS,

new LinkedBlockingQueue(),

threadFactory));

}上述代码中我们发现它有一个重载函数,传入了一个ThreadFactory 的参数,一般在我们开发中会传入我们自定义的线程创建工厂,如果不传入则会调用默认的线程工厂

我们可以看到它与 FixedThreadPool 线程池的区别仅仅是核心线程数和最大线程数改为了1,也就是说不管任务多少,它只会有唯一的一个线程去执行

如果在执行过程中发生异常等导致线程销毁,线程池也会重新创建一个线程来执行后续的任务

这种线程池非常适合所有任务都需要按被提交的顺序来执行的场景,是个单线程的串行。

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

return new ThreadPoolExecutor(0, Integer.MAX_VALUE,

60L, TimeUnit.SECONDS,

new SynchronousQueue());

}

public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {

return new ThreadPoolExecutor(0, Integer.MAX_VALUE,

60L, TimeUnit.SECONDS,

new SynchronousQueue(),

threadFactory);

}从上述代码中我们可以看到 CachedThreadPool 线程池中,最大线程数为 Integer.MAX_VALUE , 意味着他的线程数几乎可以无限增加。

因为创建的线程都是临时线程,所以他们都会被销毁,这里空闲 线程销毁时间是60秒,也就是说当线程在60秒内没有任务执行则销毁

这里我们需要注意点,它使用了 SynchronousQueue 的一个阻塞队列来存储任务,这个队列是无法存储的,因为他的容量为0,它只负责对任务的传递和中转,效率会更高,因为核心线程都为0,这个队列如果存储任务不存在意义。

ScheduledThreadPoolScheduledThreadPool 线程池是支持定时或者周期性执行任务,他的创建代码 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));

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

maxPoolSize同corePoolSize1Integer. MAX_VALUEInteger. MAX_VALUEInteger. MAX_VALUE

keepAliveTime006000

ForkJoinPoolForkJoinPool 这是一个在 JDK7 引入的新新线程池,它的主要特点是可以充分利用多核CPU , 可以把一个任务拆分为多个子任务,这些子任务放在不同的处理器上并行执行,当这些子任务执行结束后再把这些结果合并起来,这是一种分治思想。

ForkJoinPool 也正如它的名字一样,第一步进行 Fork 拆分,第二步进行 Join 合并,我们先来看一下它的类图结构

ForkJoinPool 的使用也是通过调用 submit(ForkJoinTask task)或 invoke(ForkJoinTask 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 submit = pool.submit(new Fibonacci(20));

// 获取结果,这里获取的就是异步任务的最终结果

System.out.println(submit.get());

}

}

class Fibonacci extends RecursiveTask{

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” 算法和双端队列很好地平衡了各线程的负载。

本文由AnonyStar 发布,可转载但需声明原文出处。

欢迎关注微信公账号 :云栖简码 获取更多优质文章

更多文章关注笔者博客 :云栖简码 i-code.online

java构造单例线程池_java中常见的六种线程池详解相关推荐

  1. 线程池参数详解_java中常见的六种线程池详解

    之前我们介绍了线程池的四种拒绝策略,了解了线程池参数的含义,那么今天我们来聊聊Java 中常见的几种线程池,以及在jdk7 加入的 ForkJoin 新型线程池 首先我们列出Java 中的六种线程池如 ...

  2. java break 在if 中使用_java中使用国密SM4算法详解

    前言 上次总结了一下加密算法的分类(加密算法有集中形式,各有什么不同?),现在我们用java语言实现一下SM4:无线局域网标准的分组数据算法.对称加密,密钥长度和分组长度均为128位. ps:我们既可 ...

  3. filter java 是单例的吗_JAVA 设计模式之 单例模式详解

    单例模式:(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点.单例模式是创建型模式.单例模式在现实生活中应用也非常广泛. 在 J2EE 标准中,S ...

  4. java判断线程结束_java中如何判断一个线程是否结束

    我们可以通过调用thread.Join()方法,把要判断的线程加入到当前线程中,这样可以将两个交替执行的线程合并为顺序执行的线程.如果顺利执行,则说明该线程未结束. (视频教程推荐:java视频) 比 ...

  5. java如何让线程阻塞_Java中如何使一个线程进入阻塞态?

    按我的理解,当一个线程需要获取的锁被另一个线程占用时,将进入阻塞态.但实际好像不是这样的,下面是我的代码. 在run方法中会调用MyBlock的isBlocked方法,该方法添加了synchroniz ...

  6. java后台验证不能为空_java validation 后台参数验证的使用详解

    一.前言 在后台开发过程中,对参数的校验成为开发环境不可缺少的一个环节.比如参数不能为null,email那么必须符合email的格式,如果手动进行if判断或者写正则表达式判断无意开发效率太慢,在时间 ...

  7. js去掉前后空格的函数_MySQL 中常见的字符串函数应用详解

    在前面若干章节中,我们介绍了SQL的基础与高阶语法,接下来,我们将分四个章节,介绍MySQL中常见的函数应用,大致分为如下几个章节: MySQL 字符串函数 MySQL 数字函数 MySQL 日期函数 ...

  8. java与与短路与_Java中短路运算符与逻辑运算符示例详解

    1.逻辑运算符(部分) 符号 名称 && 短路与运算符 || 短路或运算符 & 与运算符 | 或运算符 对于理工科学习者来说,逻辑运算是较为基础的概念,通常会在大一的离散数学课 ...

  9. java int byte数组_Java 中int与byte数组转换详解

    1.与运算符的理解(&): 参加运算的两个数据,按二进位进行"与"运算.如果两个相应的二进位都为1,则该位的结果值为1,否则为0.即 0&0=0:0&1=0 ...

最新文章

  1. 使用IDEA搭建第一个SpringBoot程序
  2. 【数学和算法】初识卡尔曼滤波器(六)
  3. [目录]Linux 核心系统命令目录
  4. 再赠邓超明(帮别人名字作诗)
  5. Git初学札记(七)————合并分支(merge)
  6. mysql mvcc gap lock_为什么说 MVCC 和 Gap Lock 解决了 MySQL 的幻读问题
  7. 模拟计算机怎么做,如何为具有独立模拟输出的计算机或音频系统制作8通道放大器...
  8. python公开课乐博学院_乐搏学院VIP36期全栈班学习群 - 乐搏软件教育 - 软件测试 - Powered By EduSoho...
  9. 吊炸天的Kubernetes微服务图形化管理工具:Kuboard,必须推荐给你!
  10. mysql中间件研究(tddl atlas cobar sharding-jdbc)
  11. 拦截器(Interceptor)和过滤器(Filter)区别
  12. Mixly米思齐——超声波测距控制LED灯
  13. 怎么清理计算机磁盘空间,电脑磁盘空间不足怎么清理
  14. 红帽linux能干什么,你能用Linux做什么?安徽红帽Redhat认证中心
  15. ZZNU-正约数之和(DP)
  16. 一套优秀的MES系统必须具备这3种核心功能
  17. 【CSS】单行图片与文字垂直居中
  18. 齐二TK6916/20/26/32系列数控落地铣镗床简介3
  19. ArcGIS api for javascript——查找任务-在地图上查找要素
  20. Drools7.x 学习四-规则属性

热门文章

  1. android 判断标点符号,Android文字标点符号错误
  2. flask ssti 的一些黑名单绕过姿势
  3. Egret 之消除游戏开发 PART 5-张鑫磊-专题视频课程
  4. 2022RHCE最新认证—(满分通过)
  5. 安卓中的两个build.gradle文件介绍
  6. 新人主播直播带货,该选择抖音还是快手。
  7. 由free命令引发的buff/cache思考
  8. 2018全球智慧物流峰会,一起见证一个全新物流时代的到来!
  9. 网络爬虫:魔道祖师小说
  10. 适合苹果手机的蓝牙耳机哪款好点?果粉最爱的五款综合性能超强蓝牙耳机!