并发07--线程池及Executor框架
一、JAVA中的线程池
线程池的实现原理及流程如下图所示:
如上图所示,当一个线程提交到线程池时(execute()或submit()),先判断核心线程数(corePoolSize)是否已满,如果未满,则直接创建线程执行任务;如果已满,则判断队列(BlockingQueue)是否已满,如果未满,则将线程添加到队列中;如果已满,则判断线程池(maximumPoolSize)是否已满,如果未满,则创建线程池执行任务;如果线程池已满,则交给饱和策略(RejectedExecutionHandler.rejectExcution())来处理。
可以看下线程池ThreadPoolExecutor的全参构造函数源码:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < ||
maximumPoolSize <= ||
maximumPoolSize < corePoolSize ||
keepAliveTime < )
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
对其入参释义如下:
参数 | 描述 | 作用 |
coolPoolSize | 线程核心线程数 | 当一个任务提交到线程池时,线程池会创建一个线程来执行任务,即使其他的核心线程足够执行新任务,也会创建线程,直到需要执行的任务数大于核心线程数后才不再创建;如果线程池先调用了preStartAllCoreThread()方法,则会先启动所有核心线程。 |
maximumPoolSize | 线程池最大线程数 | 如果队列满了,并且已创建的线程数小于该值,则会创建新的线程执行任务。这里需要说明一点,如果使用的队列时无界队列,那么该值无用。 |
keepAliveTime | 存活时间 | 当线程池中线程超过超时时间没有新的任务进入,则停止该线程;只会停止多于核心线程数的那几个线程。 |
unit | 线程存活的时间单位 | 可以有天、小时、分钟、秒、毫秒、微妙、纳秒 |
workQueue | 任务队列 |
用于保存等待执行任务的阻塞队列。可以选择如下几个队列:数组结构的有界队列ArrayBlockingQueue、链表结果的有界队列LinkedBlockingQueue、不存储元素的阻塞队列SynchronousQueue、一个具有优先级的无界阻塞队列PriortyBlockingQueue |
threadFactory | 创建线程的工厂 |
可以通过工厂给每个线程创建更有意义的名字。使用Guava提供的ThreadFactoryBuilder可以快速的给线程池里的线程创建有意义的名字,代码如下 new ThreadFactoryBuilder().setNameFormat("aaaaaaaa").build(); |
handler | 包和策略 |
当队列和线程都满了,说明线程池处于饱和状态,那么必须采取一种策略来处理新提交的任务。 AbortPolicy(默认),表示无法处理新任务时抛出异常。 CallerRunsPolicy:只有调用者所在线程来运行 DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务 DiscardPolicy:不处理,直接丢弃 |
上面说到,向线程池提交任务有两种方法,分别是execute()和submit(),两者的区别主要是execute()提交的是不需要有返回值的任务,而submit提交的是需要有返回值的任务,并且submit()会返回一个Furure对象,并且可以使用future.get()方法获取返回值,并且get方法会阻塞,直到有返回值。
线程池的关闭有shutdown()和shutdownNow两个方法,他们的原理是遍历线程池中的工作线程,然后逐个调用interrupt方法来中断线程,所以无法中断的线程可能永远无法终止;但是二者也有区别,shutdownNow是将线程池的状态设置为STOP,然后尝试停止所有正在执行或者暂停的线程,并返回等待执行任务列表;而shutdown只是将线程池的状态设置成SHUTDOWN,然后中断所有没有正在执行的任务。当调用这两个方法中的任何一个后,isShutdown方法就会返回true,当所有任务都已经关闭后,调用isTerminaed方法会返回true。
使用线程池时,需要从任务的性质(IO密集型还是CPU密集型或是混合型)、任务的优先级、任务的执行时常、任务的依赖性(是否依赖其他系统资源,如数据库连接等)来综合判断,比如说,CPU密集型,就可以就可以配置N+1个线程个数,其中N为CPU核数,如果是IO密集型,则可以配置2*N个线程数;如果是混合型的任务,可以将其拆分成IO密集型和CPU密集型,但是如果两个任务的执行时间相差较大,则没有必要进行拆分;优先级不同的任务可以使用优先级队列PriortyBlockingQueue来处理;依赖数据出等其它资源的线程池,比如说依赖数据库,那么就可以加大线程数量,因为在等待sql执行的时候,线程是处于空闲状态;另外,最好使用有界队列,因为无界队列,因为有界队列可以增加系统的稳定性和预警能力。
对于线程的监控,还有以下几个方法可以使用:
方法 | 描述 |
taskCount() | 线程池需要执行的任务数量 |
completedTskCount | 线程池运行过程中已经执行完毕的任务数量 |
IarestPoolSize | 线程池中曾经创建过的最大线程数 |
getPoolSize | 线程池的线程数量 |
getActiveCount | 获取活动的线程数 |
二、Exector框架
在java中,是用线程来异步执行任务,java线程的创建与销毁需要一定的开销。如果我们为每一个任务创建一个线程的话,这些线程就会消耗大量的计算资源,会使处于高负荷的应用崩溃。
在HotSpot虚拟机中,JAVA线程被一对一的映射为本地操作系统线程。JAVA线程启动时会创建一个本地操作系统线程,当该JAVA线程终止时,这个操作系统线程也会被收回,操作系统会调用多有线程并将他们分配给可用的CPU。
Executor框架的两级调度模型如上图所示,应用程序通过Executor控制上层的调度,而下层的调用由操作系统内核控制,将线程映射到硬件处理器上,下层的调用不受应用程序的控制。
关于Executor的组成部分如下所示:
元素 | 描述 |
任务 | 包括被执行任务需要实现的接口Runnable和Callable接口 |
任务的执行 | 包括任务执行机制的核心接口Executor,以及继承自Executor的ExecutorService接口。Executor接口有两个关键的实现类实现了ExecutorService接口:ThreadPoolExecutor和ScheduledThreadPoolExecutor |
异步计算的结果 | 包括接口Future和实现Future接口的FurureTask类 |
Executor框架使用示意图如下:
如上图所示,主线程首先创建实现Runnable或Callable接口的任务对象,然后把任务对象提交给ExecutorService执行,如果使用的是submit提交,执行完毕后将返回一个实现Future接口的对象,最后,主线程可以执行FutureTask.get()方法来获取返回值;主线程也可以调用FutureTask.cancel()方法来取消此任务的执行。
Executor框架的成员如下:
成员 | 描述 | 子类 | 描述 |
ThreadPoolExecutor |
通常使用工厂类Executors来创建,Executors可以创建三种类型的ThreadPoolExecutor |
固定线程数的FixedThreadPool |
适用于为了满足资源管理的需求,而需要限制当前线程数量的应用场景,它适用于负载比较重的应用。 |
单一线程的SingleThreadPool | 适用于需要保证顺序的执行各个任务,并且在任意时间点都不会有多个线程活动的场景。 | ||
根据需要创建线程的CacheThreadPool | 这是一个无界的线程池,适用于执行很多短期异步任务的小程序,或者是负载比较轻的服务器。 | ||
ScheduledThreadPoolExecutor | 通常使用工厂类Executors创建,Executors可以创建两种类型的ScheduledThreadPoolExecutor | 包含若干线程的ScheduledThreadPoolExecutor | 适用于需要多个后台线程执行周期任务,同时为了满足资源管理的需求而需要限制后台线程数量的应用场景。 |
只包含一个线程的SingleThreadScheduledExecutor | 适用于需要单个后台线程执行周期任务,同时需要保证顺序的执行各个任务的场景。 | ||
ForkJoinsPool |
newWorkStealingPool适合使用在很耗时的操作,但是newWorkStealingPool不是ThreadPoolExecutor的扩展,它是新的线程池类ForkJoinPool的扩展,但是都是在统一的一个Executors类中实现,由于能够合理的使用CPU进行对任务操作(并行操作),所以适合使用在很耗时的任务中 |
||
Future | Future接口和实现了该接口的FutureTask类来表示异步计算的结果 | ||
Runnable和Callable接口 |
Runnable和Callable接口的实现类,都可以被ThreadPoolExecutor、ScheduledThreadPool、ForkJoinThred执行;除了可以自己实现Callable接口外,我们还可以使用工厂类Executors来把一个Runnable包装成一个Callable |
ThreadPoolExecutor详解
1、ThreadPoolExecutor
(1)FixedThreadPool
构造函数如下:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
构造函数中,核心线程数和最大线程数一致,keepAliveTime为0,队列使用的是无界阻塞队列LinkedBlockingQueue(最大值是Integer.MAX_VALUE);
核心线程数和最大线程数保持一致,表明:如果队列满了之后,不会再创建新的线程;
keepAliveTime为0,表明:如果运行线程数大于核心线程数时,如果线程执行完毕,空闲线程立刻被终止;
使用无界阻塞队列,表明:当运行线程到达核心线程数时,不会再创建线程,只会将任务加入阻塞队列;因此最大线程数参数无效;因此keepAliveTime参数无效;且不会拒绝任务(既不会执行包和策略)
(2)SingleThreadExecutor
构造函数如下:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(, ,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
构造函数中,核心线程数和最大线程数均为1,keepAliveTime为0,队列使用的是无界阻塞队列LinkedBlockingQueue(最大值是Integer.MAX_VALUE)
除了固定了核心线程数和最大线程数为1外,其余的参数均与FixedThreadPool一致,那么就是只有一个线程会反复循环从阻塞队列中获取任务执行
(3)CacheThreadPool
构造函数如下:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
构造函数中,核心线程数为0,最大线程数为Integer.MAX_VALUE,意味着无界,keepAliveTime为60秒,阻塞队列使用没有存储空间的SynchronousQueue
核心线程数为0,最大线程数为无界,表明:只要队列满了,就会创建新的线程放入线程池
使用没有存储空间的SynchronousQueue表明:线程提交的速度高于线程被消费的速度,那么线程会被不断的创建,最终会因为线程创建过多而耗尽CPU和内存资源
2、ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor的运行机制如下:
(1)当调用ScheduledTreadPoolExecutor的scheduleAtFixedRate()方法或者scheduleWithFixedDelay()方法时会向ScheduledThreadPoolExecutor的DelayQueue添加一个实现了RunnableScheduledFuture接口的ScheduledFutureTask
(2)线程池中的线程从DelayQueue中获取ScheduledFutureTask,然后执行任务。
ScheduledFutureTask主要包含以下三个成员变量
成员变量 | 描述 |
long time | 表示这个任务要被执行的时间 |
long sequenceNumber | 表示该任务被添加到ScheduledThreadPoolExecutor中的序号 |
long period | 表示任务执行的间隔周期 |
DelayQueue封装了一个PriorityQueue,当添加任务时,这个PriorityQueue会对队列中的ScheduledFutureTask进行排序,time最小的在最前面(最先被执行),如果time一致,就比较sequenceNumber,sequenceNumber小的排在前面。
当线程执行任务时,先从DelayQueue队列中获取已经到期的任务(time大于当前时间),然后执行该任务,执行完毕后,根据任务的执行周期,修改任务下次的执行时间time,并重新将任务添加到DelayQueue
FutureTask详解
Future接口和实现该接口的FutureTask类,代表异步计算的结果。
FutureTask的使用方法是将其交给Executor执行,也可以通过ExecutorService.submit()方法返回一个FutureTask,然后执行FutureTask.get()方法或FutureTask.cancel()方法,除此之外,还可以但是使用FutureTask。
FutureTask有三种状态:未启动(FutureTask.run()没有被执行之前的状态)、已启动(FutureTask.run()方法执行过程中)、已完成(FutureTask.run()方法执行完成或被取消),这三种状态的流转如下图所示:
FutureTask的实现是基于AQS(AbstractQueuedSynchrouizer)来实现的,之前已经说过,每一个基于AQS实现的同步器都会至少包含一个acquire操作和至少一个release操作。AQS被作为模板方法模式的基础类提供给FutureTask的内部子类Sync实现了AQS的tryAcquireShared(int)方法和tryReleaseShared(int)方法,Sync通过这两个方法来检查和更新同步状态。
FutureTask涉及示意图如下图所示:
如上图所示,FutureTask.get()方法会调用AQS的acquireSharedInterruptibly(int)方法,该方法首先会回调在子类Sync中的tryAcquireShared()方法来判断acquire操作是否成功(state状态状态是否为执行完成RAN或取消状态CANCELED&runner不为null),如果成功则get()方法立刻返回,如果失败则到线程等待队列中去等待其他线程执行release操作;当其他线程执行release操作(比如FutureTask.run()或FutureTask.cancel())唤醒当前线程后,当前线程再次执行tryAcquireShared()将返回正值1,当前线程将离开线程等待队列并唤醒它的后继线程。
Run方法执行过程如下:
执行在构造函数中指定的任务(Callable.call()),然后以原子方式来更新状态(调用AQS.compareAndSetState(int expect, int update),设置state的状态为RAN),如果这个原子操作成功,就设置代表计算结果的变量result的值为Callable.call()的返回值,然后调用AQS.release(int)。
AQS.rease首先会调用子类Sync中实现的tryReleaseShared方法来执行release操作(设置运行任务的线程为null,然后返回false),然后唤醒等待队列中的第一个线程。
最后调用Future.done()方法。
并发07--线程池及Executor框架的更多相关文章
- 线程池之Executor框架
线程池之Executor框架 Java的线程既是工作单元,也是执行机制.从JDK5开始,把工作机单元和执行机制分离开来.工作单元包括Runnable和Callable,而执行机制由Executor框架 ...
- Java多线程学习(八)线程池与Executor 框架
目录 历史优质文章推荐: 目录: 一 使用线程池的好处 二 Executor 框架 2.1 简介 2.2 Executor 框架结构(主要由三大部分组成) 2.3 Executor 框架的使用示意图 ...
- 线程池及Executor框架
1.为什么要使用线程池? 诸如 Web 服务器.数据库服务器.文件服务器或邮件服务器之类的许多服务器应用程序都面向处理来自某些远程来源的大量短小的任务.请求以某种方式到达服务器,这种方式可能是通过 ...
- java并发编程(十七)Executor框架和线程池
转载请注明出处:http://blog.csdn.net/ns_code/article/details/17465497 Executor框架简介 在Java 5之后,并发编程引入了一堆新的启动 ...
- java并发:线程池、饱和策略、定制、扩展
一.序言 当我们需要使用线程的时候,我们可以新建一个线程,然后显式调用线程的start()方法,这样实现起来非常简便,但在某些场景下存在缺陷:如果需要同时执行多个任务(即并发的线程数量很多),频繁地创 ...
- 并发编程学习笔记(15)----Executor框架的使用
Executor执行已提交的 Runnable 任务的对象.此接口提供一种将任务提交与每个任务将如何运行的机制(包括线程使用的细节.调度等)分离开来的方法.通常使用 Executor 而不是显式地创建 ...
- 并发编程-线程池&J.U.C
8. 共享模型之工具 8.1 线程池 池化技术相比大家已经屡见不鲜了,线程池.数据库连接池.Http 连接池等等都是对这个思想的应用.池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率 ...
- (CSDN迁移) JAVA多线程实现-可控最大并发数线程池(newFixedThreadPool)
上篇文章中介绍了单线程化线程池newSingleThreadExecutor,可控最大并发数线程池(newFixedThreadPool)与其最大的区别是可以通知执行多个线程,可以简单的将newSin ...
- Java并发(基础知识)—— Executor框架及线程池
在Java并发(基础知识)—— 创建.运行以及停止一个线程中讲解了两种创建线程的方式:直接继承Thread类以及实现Runnable接口并赋给Thread,这两种创建线程的方式在线程比较少的时候是没有 ...
随机推荐
- Chisel3 - model - UserModule commands
https://mp.weixin.qq.com/s/0ECca6XyFyEri0B4ckOZ4A 介绍UserModule类中,如何管理构建硬件模型所需的命令. 1. _comma ...
- java实现猜生日
** 猜生日** 今年的植树节(2012年3月12日),小明和他的叔叔还有小伙伴们一起去植树.休息的时候,小明的同学问他叔叔多大年纪,他叔叔说:"我说个题目,看你们谁先猜出来!" ...
- Redis企业级数据备份与恢复方案
一.持久化配置 RBD和AOF建议同时打开(Redis4.0之后支持) RDB做冷备,AOF做数据恢复(数据更可靠) RDB采取默认配置即可,AOF推荐采取everysec每秒策略 AOF和RDB还不 ...
- Python——day3
看到右边的时钟了吗? 我想世界最公平的一件事就是每个人的每一小时.每一天.每一年都是相同的,每个人的时间都是一样的. 一直保持温热感是一件很了不起的事,加油,屏幕前的你和我. 明天,还在等你 回顾d ...
- Java培训Day01——制作疫情地图(一)
一.前言 此次培训,是为期三天的网上培训.最终的目的是制作出疫情地图.首先我们来看看主要的讲课内容大纲. Day1 |-Java语法学习(个人感觉讲得还可以,主要围绕本次培训作出的讲解,没有像网上的基 ...
- 我们为什么要用hibernate
1.hibernate对JDBC访问数据库的代码做了一个封装,简化了数据访问繁琐的代码. 2.hibernate的性能非常好,因为它是个轻量级框架.映射的灵活性很好,它支持各种关系型数据库,从一对一到 ...
- web静态页面资源访问路径问题
我使用的是idea,今天搭建一个项目时遇见了css和js路径错误,导致浏览器获取不到资源路径 这是我最开始写的路径 <link href="/main/loginMain.css&qu ...
- 简单5步,轻松debug K8S服务!
作者: Ram Rai,性能.可扩展性以及软件架构的爱好者 原文链接: https://medium.com/better-programming/debug-your-kubernetes-serv ...
- mysql 双机互备份
//1.创建用户CREATE USER 'dump'@'%' IDENTIFIED BY 'dump'; //2.开放权限GRANT ALL ON *.* TO 'dump'@'%'; //3.刷新权 ...
- 问题解决:psql: could not connect to server: No such file or directory Is the server running locally and accepting connections on Unix domain socket "/var/run/postgresql/.s.PGSQL.5432"?
错误提示: psql: could not connect to server: No such file or directory Is the server running locally and ...