JUC系列之线程池的使用
JUC系列文章目录
JUC系列往期文章
文章目录
- JUC系列文章目录
- 前言
- 一、线程池
- 二、线程池构造函数的参数
- 三、线程池处理任务流程
- 四、如何自定义合适的线程池
- 五、如何正确关闭线程池
- 五、线程池复用原理,源码分析
前言
前面几天忙着写论文好久没更新文章了,不能这么懈怠,还是得随时进行学习的反馈。虽然前面的坑还没填完,但我现在又准备开新坑了o( ̄︶ ̄)o。目前计划开个新专栏专门整理一下java.util.concurrent,Java并发包,也当做是复习,这一篇呢就是这个新系列的第一篇文章,话不多说,直接开冲!
一、线程池
对于线程池,我的理解是,它是一个对线程进行管理的中介,在一些多线程背景下的操作,如果每来一个新的任务都去创建一个新的线程去进行处理,这样过多的线程会去占用系统的内存,另外也涉及线程创建和销毁带来的开销,这样的开销是我们不想看到的,所以我们需要线程池这样一个中介去对我们的线程进行一个管理。
二、线程池构造函数的参数
前面刷牛客网面经的时候有一道题我记得就是说说线程池的创建有哪些参数,这里主要是下面表格记录的七个参数
参数名 | 类型 | 含义 |
---|---|---|
corePoolSize | int | 线程池初始化时,默认的线程数是0,当有任务提交后,开始创建核心线程去执行任务 |
maximumPoolSize | int | 当线程数达到核心线程数时且任务队列满了后,开始创建非核心线程执行任务。线程池能够容纳同时执行的最大线程数 |
keepAliveTime | long | 多余的空闲线程存活时间,当空间时间达到keepAliveTime值时,多余的线程会被销毁直到只剩下corePoolSize个线程为止。 |
unit | - | keepAliveTime的单位 |
workQueue | - | 任务队列,被提交但尚未被执行的任务 |
threadFactory | - | 表示生成线程池中工作线程的线程工厂,用户创建新线程,一般用默认即可 |
handler | - | 拒绝策略,表示当线程队列满了并且工作线程大于等于线程池的最大显示数(maxnumPoolSize)时如何来拒绝请求执行的runnable的策略 |
前面四个参数都很好理解,这里主要说一下后面三个参数。
workQueue
:
主要介绍一下5种阻塞队列。
ArrayBlockingQueue
是基于数组的有界阻塞队列,按照FIFO排序,新来的队列会放到队列尾部,有界的数组可以防止资源被耗尽问题,当线程达到了核心线程数,再来任务的时候就放到队列的尾部,当队列满了的时候,则继续创建非核心线程,如果线程数量达到了maxPoolSize,则会执行拒绝策略。
LinkedBlockingQueue
是基于链表的无界阻塞队列(最大容量是Integer.MAX),按照FIFO排序,当线程池中线程数量达到核心线程数时,继续来了新任务会一直存放到队列中,而不会创建新线程.因此使用此队列时,maxPoolSize是不起做的。
SynchronousQueue
是一个不缓存任务的阻塞队列,当来了新任务的时候,不会缓存到队列中,而是直接被线程执行该任务,如果没有核心线程可用就创建新线程去执行任务,达到了maxPoolSize时,就执行拒绝策略。
PriorityBlockingQueue
是一个具有优先级的无界阻塞队列,优先级通过参数Comparator实现。
DelayedWorkQueu
队列的特点是内部的任务并不是按照放入的时间排序,而是会按照延迟的时间长短对任务进行排序,内部采用的是“堆”数据结构.而且它也是一个无界队列。
threadFactory
:
ThreadFactory是一个线程工厂,负责生产线程去执行任务,默认的线程工厂,创建的线程会在同一个线程组,并且拥有一样的优先级,且都不是守护线程,我们也可自定义线程工厂,以便给线程自定义名字。
handler
拒绝策略是当线程池中任务达到了队列最大容量,且线程数量也达到了最大maxPoolSize的时候,如果继续有新任务来了,则执行这个拒绝策略来处理新来的任务,系统默认的拒绝策略有以下4种。
AbortPolicy:为线程池默认的拒绝策略,该策略直接抛异常处理。
DiscardPolicy:直接抛弃不处理。
DiscardOldestPolicy:丢弃队列中最老的任务,然后尝试把这次拒绝的任务放入队列。
CallerRunsPolicy:将任务分配给当前执行execute方法线程来处理,就是谁提交的任务,谁负责执行任务,这样任务不会丢失,而且执行任务比较费时,那么提交任务的线程也会被占用,就可以减缓任务提交速度。
三、线程池处理任务流程
总结一下,当提交任务后,线程池首先会检查当前线程数,如果当前线程数小于核心线程数,则新建核心线程数量的线程并执行任务。随着任务不断增加,线程数达到了核心线程数的数量,此时任务依然在增加,那么新来的任务将会放到workQueue等待队列中,等核心线程执行完任务后重新从队列中提取出等待被执行的任务。如果已经达到了核心线程数,且任务队列也满了,则线程池就会继续创建线程来执行任务,如果任务不断提交,线程池会持续创建线程直到达到maximumPoolSize最大线程数,当达到了最大线程数后,任务仍不断提交,那么此时就超过了线程池的最大处理能力,这个时候线程池就会拒绝处理这些任务,处理策略就是handler。
四、如何自定义合适的线程池
首先,如果我们直接使用JDK封装好的构造函数自动创建线程会发生什么,答案是可能由于使用无界工作队列导致任务堆积或者直接创建新的线程转发任务导致过多线程资源,发生内存溢出的问题。
如果自定义合适的线程池呢?首先我们要调整线程池中的线程数量以便充分并合理的使用CPU和内存资源,从而最大限度的提高性能。
CPU密集型任务
如果任务是一些列比较消耗CPU资源的任务,比如加密、解密、压缩、计算等,那么最佳线程数是CPU核心数的1~2倍,过多很导致占用大量CPU资源,这时每个CPU的核心工作基本都是满负荷,设置过多的线程会造成不必要的上下文切换,而导致性能下降,而且在同一台机器上,我们还要考虑到其他会占用较多CPU资源的程序运行,然后做整体平衡。
耗时IO任务
例如数据库、文件的读写,网络通信等任务,这种任务的特点是不会消耗很多CPU资源,但是IO操作很费时.这个时候可以设置最大线程数一般会大于CPU核心线程数很多倍,因为IO速度相比于CPU速度比较慢,我们设置较少的线程数,就会浪费CPU资源,如果设置更多的线程数,那么一部分线程正在等待IO的时候,他们此时不需要CPU计算,就能有更多线程去执行IO操作,任务队列中的等待任务就会减少,更合理的利用了资源。
java并发编程实战中有推荐:线程数 = CPU核心数 *(1+平均等待时间/平均工作时间),我们可以通过这个式子计算出一个合理的线程数量,同时也可以根据进行压测、监控jvm的线程情况等方式,确定线程数,更合理的利用资源。
总结以上特点可以得出以下几点:
线程的平均工作时间所占比例越高,就需要越少线程
线程的平均等待时间所占比例越高,就需要越多的线程
五、如何正确关闭线程池
这里介绍5种关闭线程的方法。
void shutdown()
它可以安全的关闭一个线程池,调用shutdown()方法后,线程池不会立刻关闭,而是等执行完正在执行的任务和队列中等待的任务后才彻底关闭,而且调用shutdown()方法后,如果还有新的任务继续到来,那么线程池会根据拒绝策略直接拒绝后面来的新任务.
boolean isShutdown()
这个方法可以返回ture或者false来判断是否已经开始了关闭工作,也就是是否执行了shutdown或者shutdownNow方法,调用isShutdown()方法后如果返回true,并不代表线程池已经彻底关闭了,仅仅代表开始了关闭流程,仍然可能有线程正在执行任务,队列里也可能有任务等待被执行.
boolean isTerminated()
这个方法可以检测是否真正关闭了,不仅代表线程池是否已经关闭,同时也代表线程池中的所有任务是否已经都执行完毕,比如已经调用了shutdown()方法,但是有一个线程正在执行任务,则此时调用isShutdown方法返回true,而调用isTerminated方法便返回false,因为线程池中还有任务再执行,线程池没有真正关闭,直到所有线程都执行完毕,任务都执行完毕,再调用isTermainted就返回ture.
boolean awaitTermination(long timeout,TimeUnit unit),throws IntereuptedException
awaitTermination并不是用来关闭线程池的,而是用来判断线程池状态的,参数需要传入一个时间,如果我们设置10秒钟,那么会有以下几种情况:
–
等待期间,线程池已经关闭且所有提交的任务都执行完毕,那么方法就返回ture,相当于线程池真正关闭了.
–
等待时间超时后,第一种情况未发生,那么方法返回false.
–
等待时间中,执行任务的线程被中断了,方法会抛出InterruptedException异常.
–
所以综上可以看出,调用 awaitTermination 方法后当前线程会尝试等待一段指定的时间,如果在等待时间内,线程池已关闭并且内部的任务都执行完毕了,也就是说线程池真正“终结”了,那么方法就返回 true,否则超时返回 fasle,我们则可以根据 awaitTermination() 返回的布尔值来判断下一步应该执行的操作。
List shutdownNow()
调用shutdownNow()方法后,首先会给所有线程池中的线程发送interrupt中断信号,尝试中断这些任务的执行,然后和任务队列中在等待被执行的任务转移到一个List中并返回,我们可以再根据List做一些操作,shutdownNow() 的源码如下所示:
五、线程池复用原理,源码分析
线程池对 Thread 进行了封装,并不是每次执行任务都会调用 Thread.start() 来创建新线程,而是让每个线程去执行一个“循环任务”,在这个“循环任务”中,不停地检查是否还有任务等待被执行,如果有则直接去执行这个任务,也就是调用任务的 run 方法。
public void execute(Runnable command) { //如果传入的Runnable的空,就抛出异常if (command == null) throw new NullPointerException();int c = ctl.get();/*** addWorker 方法的主要作用是在线程池中创建一个线程并执行* 第一个参数是传入的任务,它的第二个参数是个布尔值,这里的布尔值的含义是以核心线程数为界限还是以最大线程数为界限进行是否新增线程的判断* addWorker() 方法如果返回 true 代表添加成功,如果返回 false 代表添加失败*/if (workerCountOf(c) < corePoolSize) { if (addWorker(command, true)) return;c = ctl.get();} /*** 当前线程数大于或等于核心线程数或者 addWorker 失败了* 检查线程池状态是否为 Running* 如果线程池状态是 Running 就把任务放入任务队列中;* 如果线程池已经不处于 Running 状态,说明线程池被关闭,那么就移除刚刚添加到任务队列中的任务,并执行拒绝策略。*/if (isRunning(c) && workQueue.offer(command)) { int recheck = ctl.get();if (! isRunning(recheck) && remove(command)) reject(command);/**当任务被添加进来之后就需要防止没有可执行线程的情况发生(比如之前的线程被回收了或意外终止了),所以此时如果检查当前线程数是否为0*/else if (workerCountOf(recheck) == 0) addWorker(null, false);} /**线程池不是 Running 状态或线程数大于或等于核心线程数并且任务队列已经满了,所以此时需要添加新线程,直到线程数达到“最大线程数”*/else if (!addWorker(command, false)) reject(command);
}
JUC系列之线程池的使用相关推荐
- Java juc系列6 —— 线程池
Java JUC系列目录链接 Java 线程池核心原理解析 Java线程池的基础用法 创建和使用 为什么需要线程池 线程的生命周期[^1] 新建 就绪 运行 休眠 终止 使用线程的代价 线程池帮我们做 ...
- JUC系列(六) 线程池
- 【重难点】【JUC 05】线程池核心设计与实现、线程池使用了什么设计模式、要你设计的话,如何实现一个线程池
[重难点][JUC 05]线程池核心设计与实现.线程池使用了什么设计模式.要你设计的话,如何实现一个线程池 文章目录 [重难点][JUC 05]线程池核心设计与实现.线程池使用了什么设计模式.要你设计 ...
- JUC多线程:线程池的创建及工作原理 和 Executor 框架
一.什么是线程池: 线程池主要是为了解决 新任务执行时,应用程序为任务创建一个新线程 以及 任务执行完毕时,销毁线程所带来的开销.通过线程池,可以在项目初始化时就创建一个线程集合,然后在需要执行新任务 ...
- 【JUC】动态线程池
一.背景 需求:动态调整参数.细粒度监控.秒级监控 线程池参数调优,需要不断的进行测试,判断内存占用等因素,调优没法热部署 别再纠结线程池大小了,没有固定公式 二.美团DynamicTp dynami ...
- 并发编程系列之线程池工厂类:Executors
前言 上节讲了讲自定义线程池,今天我们来聊聊线程池框架,在实际开发中我们还是基本使用线程框架Executor给我们提供的一些工具类,Java提供的Executor都在JUC(java.util.con ...
- 侠客岛--多线程系列之线程池(十二)
文章目录 线程池原理 一.为什么要使用线程池 二.线程池的原理 1. ThreadPoolExecutor提供的构造方法 1.1 构造方法 1.2 构造方法参数 2. ThreadPoolExecut ...
- java 批量插入clob_SpringBoot系列(16)线程池Executors并发编程之批量查询-插入数据
在上篇文章中Debug给大家分享介绍了"Java线程池-多线程的其中一种应用场景~广播式给所有有效用户发送邮件(通知)",本篇文章我们将继续向前迈进,继续介绍并实战"线程 ...
- 并发编程系列---【线程池七大核心参数】
一.七大核心参数 1.corePoolSize 核心线程数 2.maximumPoolSize 最大线程池参数 ...
最新文章
- JS中字符串的相关操作
- Linux学习:gcc 编译其他常用参数
- java加密字符串,可解密
- CORS 跨域-同源策略
- Java中父类的私有数据和静态数据在内存中是如何存储的?
- ROS有三个层级的概念,分别是:文件系统级、计算图级和开源社区级
- php的web表单系统源码毕设_从业十多年看了千百套Java毕设项目,整理出100个精品!免费分享...
- HashMap中hash(Object key)原理(hashcode >>> 16)
- python中如何获取类的属性,python – 获取类的属性
- java 截取两个字符之间的字符串_java里面如何截取两个关键字中间的字符串?
- batchupdate一次多少条合适_中药材半夏地下茎块膨大剂,中药材半夏一次冲施多少肥料合适?...
- 流媒体有哪些播放方式?流媒体视频三种播放方式介绍
- OSChina 周三乱弹 ——你是有多寂寞啊,看光头强都……
- android 实现qq动画,Android实现仿QQ登录界面背景动画效果
- python将数值存入excel指定单元格
- 信创舆情一线--工信部开展APP侵害用户权益专项整治行动
- 共克时艰|链下思考系列之一区块链能做点什么
- DBA需要掌握的技能和经验
- IoTGateway 国内开源工业 IoT 物联网网关
- 岁末忆今朝,辞旧话新潮——心灵与技术的聚合