什么是线程池?
诸如web服务器、数据库服务器、文件服务器和邮件服务器等许多服务器应用都面向处理来自某些远程来源的大量短小的任务。构建服务器应用程序的一个过于简单的模型是:每当一个请求到达就创建一个新的服务对象,然后在新的服务对象中为请求服务。但当有大量请求并发访问时,服务器不断的创建和销毁对象的开销很大。所以提高服务器效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁,这样就引入了“池”的概念,“池”的概念使得人们可以定制一定量的资源,然后对这些资源进行复用,而不是频繁的创建和销毁。
线程池是预先创建线程的一种技术。线程池在还没有任务到来之前,创建一定数量的线程,放入空闲队列中。这些线程都是处于睡眠状态,即均为启动,不消耗CPU,而只是占用较小的内存空间。当请求到来之后,缓冲池给这次请求分配一个空闲线程,把请求传入此线程中运行,进行处理。当预先创建的线程都处于运行状态,即预制线程不够,线程池可以自由创建一定数量的新线程,用于处理更多的请求。当系统比较闲的时候,也可以通过移除一部分一直处于停用状态的线程。
在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。在Java中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收。
所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁。如何利用已有对象来服务就是一个需要解决的关键问题,其实这就是一些”池化资源”技术产生的原因。
在开发程序的过程中,很多时候我们会遇到遇到批量执行任务的场景,当各个具体任务之间互相独立并不依赖其他任务的时候,我们会考虑使用并发的方式,将各个任务分散到不同的线程中进行执行来提高任务的执行效率。
我们会想到为每个任务都分配一个线程,但是这样的做法存在很大的问题:
1、资源消耗:首先当任务数量庞大的时候,大量线程会占据大量的系统资源,特别是内存,当线程数量大于CPU可用数量时,空闲线程会浪费造成内存的浪费,并加大GC的压力,大量的线程甚至会直接导致程序的内存溢出,而且大量线程在竞争CPU的时候会带来额外的性能开销。如果CPU已经足够忙碌,再多的线程不仅不会提高性能,反而会降低性能。
2、线程生命周期的开销:线程的创建和销毁都是有代价的,线程的创建需要时间、延迟处理的请求、需要JVM和操作系统提供一些辅助操作。如果请求特别庞大,并且任务的执行特别轻量级(比如只是计算1+1),那么对比下来创建和销毁线程代价就太昂贵了。
3、稳定性:如资源消耗中所说,如果程序因为大量的线程抛出OutOfMemoryEorror,会导致程序极大的不稳定。
既然为每个任务分配一个线程的做法已经不可行,我们考虑的代替方法中就必须考虑到,1、线程不能不能无限制创建,数量必须有一个合适的上限。2、线程的创建开销昂贵,那我们可以考虑重用这些线程。理所当然,池化技术是一项比较容易想到的替代方案(马后炮),线程的池化管理就叫线程池。
线程池简介
多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力。
假设一个服务器完成一项任务所需时间为:T1 创建线程时间,T2 在线程中执行任务的时间,T3 销毁线程时间。
如果:T1 + T3 远大于 T2,则可以采用线程池,以提高服务器性能。
一个线程池包括以下四个基本组成部分:
1、线程池管理器(ThreadPool):用于创建并管理线程池,包括创建线程池,销毁线程池,添加新任务;
2、工作线程(PoolWorker):线程池中线程,在没有任务时处于等待状态,可以循环的执行任务;
3、任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;
4、任务队列(taskQueue):用于存放没有处理的任务。提供一种缓冲机制。
线程池技术正是关注如何缩短或调整T1,T3时间的技术,从而提高服务器程序性能的。它把T1,T3分别安排在服务器程序的启动和结束的时间段或者一些空闲的时间段,这样在服务器程序处理客户请求时,不会有T1,T3的开销了。
线程池不仅调整T1,T3产生的时间段,而且它还显著减少了创建线程的数目,看一个例子:
假设一个服务器一天要处理50000个请求,并且每个请求需要一个单独的线程完成。在线程池中,线程数一般是固定的,所以产生线程总数不会超过线程池中线程的数目,而如果服务器不利用线程池来处理这些请求则线程总数为50000。一般线程池大小是远小于50000。所以利用线程池的服务器程序不会为了创建50000而在处理请求时浪费时间,从而提高效率。
代码实现中并没有实现任务接口,而是把Runnable对象加入到线程池管理器(ThreadPool),然后剩下的事情就由线程池管理器(ThreadPool)来完成了。
为什么要用线程池:
1、减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
2、可以根据系统的承受能力,调整线程池中工作线程的数目,防止因为消耗过多的内存而把服务器累趴下(每个线程需要大于1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService。
线程池的作用:
线程池作用就是限制系统中执行线程的数量。
根据系统的环境情况,可以自动或手动设置线程数量,达到运行的最佳效果;少了浪费了系统资源,多了造成系统拥挤效率不高。用线程池控制线程数量,其他线程排队等候。一个任务执行完毕,再从队列的中取最前面的任务开始执行。若队列中没有等待进程,线程池的这一资源处于等待。当一个新任务需要运行时,如果线程池中有等待的工作线程,就可以开始运行了;否则进入等待队列。
线程池的实现原理
提交一个任务到线程池中,线程池的处理流程如下:
1、判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。
2、线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。
3、判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。
使用场景
1、异步处理日志,这个是比较场景的采用线程池来解决的
2、定时任务,定时对数据库备份、定时更新redis配置等,定时发送邮件
3、数据迁移
这些常见的一些场景我们就应该优先来考虑线程池来解决
线程池本质的概念就是一堆线程一起完成一件事情。
线程池技术要点
从内部实现上看,线程池技术可主要划分为如下6个要点实现:
工作者线程worker:即线程池中可以重复利用起来执行任务的线程,一个worker的生命周期内会不停的处理多个业务job。线程池“复用”的本质就是复用一个worker去处理多个job,“流控“的本质就是通过对worker数量的控制实现并发数的控制。通过设置不同的参数来控制 worker的数量可以实现线程池的容量伸缩从而实现复杂的业务需求。
待处理工作job的存储队列:工作者线程workers的数量是有限的,同一时间最多只能处理最多workers数量个job。对于来不及处理的job需要保存到等待队列里,空闲的工作者work会不停的读取空闲队列里的job进行处理。基于不同的队列实现,可以扩展出多种功能的线程池,如定制队列出队顺序实现带处理优先级的线程池、定制队列为阻塞有界队列实现可阻塞能力的线程池等。流控一方面通过控制worker数控制并发数和处理能力,一方面可基于队列控制线程池处理能力的上限。
线程池初始化:即线程池参数的设定和多个工作者workers的初始化。通常有一开始就初始化指定数量的workers或者有请求时逐步初始化工作者两种方式。前者线程池启动初期响应会比较快但造成了空载时的少量性能浪费,后者是基于请求量灵活扩容但牺牲了线程池启动初期性能达不到最优。
处理业务job算法:业务给线程池添加任务job时线程池的处理算法。有的线程池基于算法识别直接处理job还是增加工作者数处理job或者放入待处理队列,也有的线程池会直接将job放入待处理队列,等待工作者worker去取出执行。
workers的增减算法:业务线程数不是持久不变的,有高低峰期。线程池要有自己的算法根据业务请求频率高低调节自身工作者workers的 数量来调节线程池大小,从而实现业务高峰期增加工作者数量提高响应速度,而业务低峰期减少工作者数来节省服务器资源。增加算法通常基于几个维度进行:待处 理工作job数、线程池定义的最大最小工作者数、工作者闲置时间。
线程池终止逻辑:应用停止时线程池要有自身的停止逻辑,保证所有job都得到执行或者抛弃。
线程池的优点:
1.重用线程池中的线程,减少因对象创建,销毁所带来的性能开销;
2.能有效的控制线程的最大并发数,提高系统资源利用率,同时避免过多的资源竞争,避免堵塞;
3.能够多线程进行简单的管理,使线程的使用简单、高效。
4.减少频繁的创建和销毁线程(由于线程创建和销毁都会耗用一定的内存)
5.线程池也是多线程,充分利用CPU,提高系统的效率
6.线程是稀缺资源,使用线程池可以减少创建和销毁线程的次数,每个工作线程都可以重复使用。
7.可以根据系统的承受能力,调整线程池中工作线程的数量,防止因为消耗过多内存导致服务器崩溃。
8.线程复用
9.控制最大并发数
10.管理线程
线程池的工作过程如下:
线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
当调用 execute() 方法添加一个任务时,线程池会做如下判断:
如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常RejectExecutionException。
当一个线程完成任务时,它会从队列中取下一个任务来执行。
当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。
执行流程
调用ThreadPoolExecutor的execute提交线程,首先检查CorePool,如果CorePool内的线程小于CorePoolSize,新创建线程执行任务。
如果当前CorePool内的线程大于等于CorePoolSize,那么将线程加入到BlockingQueue。
如果不能加入BlockingQueue,在小于MaxPoolSize的情况下创建线程执行任务。
如果线程数大于等于MaxPoolSize,那么执行拒绝策略。
配置线程池的大小
一般需要根据任务的类型来配置线程池大小:
如果是CPU密集型任务,就需要尽量压榨CPU,参考值可以设为 NCPU+1;
如果是IO密集型任务,参考值可以设置为2*NCPU。
当然,这只是一个参考值,具体的设置还需要根据实际情况进行调整,比如可以先将线程池大小设置为参考值,再观察任务运行情况和系统负载、资源利用率来进行适当调整。
线程池的注意事项
虽然线程池是构建多线程应用程序的强大机制,但使用它并不是没有风险的。在使用线程池时需注意线程池大小与性能的关系,注意并发风险、死锁、资源不足和线程泄漏等问题。
(1)线程池大小。多线程应用并非线程越多越好,需要根据系统运行的软硬件环境以及应用本身的特点决定线程池的大小。一般来说,如果代码结构合理的话,线程数目与CPU 数量相适合即可。如果线程运行时可能出现阻塞现象,可相应增加池的大小;如有必要可采用自适应算法来动态调整线程池的大小,以提高CPU 的有效利用率和系统的整体性能。
(2)并发错误。多线程应用要特别注意并发错误,要从逻辑上保证程序的正确性,注意避免死锁现象的发生。
(3)线程泄漏。这是线程池应用中一个严重的问题,当任务执行完毕而线程没能返回池中就会发生线程泄漏现象。
是否使用线程池就一定比使用单线程高效呢?
答案是否定的,比如Redis就是单线程的,但它却非常高效,基本操作都能达到十万量级/s。从线程这个角度来看,部分原因在于:
多线程带来线程上下文切换开销,单线程就没有这种开销
锁
当然“Redis很快”更本质的原因在于:Redis基本都是内存操作,这种情况下单线程可以很高效地利用CPU。而多线程适用场景一般是:存在相当比例的IO和网络操作。
所以即使有上面的简单估算方法,也许看似合理,但实际上也未必合理,都需要结合系统真实情况(比如是IO密集型或者是CPU密集型或者是纯内存操作)和硬件环境(CPU、内存、硬盘读写速度、网络状况等)来不断尝试达到一个符合实际的合理估算值。
线程使用要点:
线程数量要点:
如果运行线程的数量少于核心线程数量,则创建新的线程处理请求
如果运行线程的数量大于核心线程数量,小于最大线程数量,则当队列满的时候才创建新的线程
如果核心线程数量等于最大线程数量,那么将创建固定大小的连接池
如果设置了最大线程数量为无穷,那么允许线程池适合任意的并发数量
线程空闲时间要点:
当前线程数大于核心线程数,如果空闲时间已经超过了,那该线程会销毁。
排队策略要点:
同步移交:不会放到队列中,而是等待线程执行它。如果当前线程没有执行,很可能会新开一个线程执行。
无界限策略:如果核心线程都在工作,该线程会放到队列中。所以线程数不会超过核心线程数
有界限策略:可以避免资源耗尽,但是一定程度上减低了吞吐量
当线程关闭或者线程数量满了和队列饱和了,就有拒绝任务的情况了:
拒绝任务策略:
直接抛出异常
使用调用者的线程来处理
直接丢掉这个任务
丢掉最老的任务
线程池结构
线程生命周期
在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态。
Thread通过new来新建一个线程,这个过程是是初始化一些线程信息,如线程名,id,线程所属group等,可以认为只是个普通的对象。调用Thread的start()后Java虚拟机会为其创建方法调用栈和程序计数器,同时将hasBeenStarted为true,之后调用start方法就会有异常。
处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了。至于该线程何时开始运行,取决于JVM里线程调度器的调度。当线程获取cpu后,run()方法会被调用。不要自己去调用Thread的run()方法。之后根据CPU的调度在就绪——运行——阻塞间切换,直到run()方法结束或其他方式停止线程,进入dead状态。
源码解析
Java中线程池主要是并发包java.util.concurrent 中 ThreadPoolExecutor这个类实现的。
线程池框架Executor
java中的线程池是通过Executor框架实现的,Executor 框架包括类
Executor
Executors
ExecutorService
ThreadPoolExecutor
Callable
Future
FutureTask
(1) Executor: 所有线程池的接口,只有一个方法。
public interface Executor {
void execute(Runnable command);
}
execute执行方法
execute执行方法分了三步,以注释的方式写在代码上了~
public void execute(Runnable command) { if (command == null) throw new NullPointerException(); int c = ctl.get(); //如果线程池中运行的线程数量<corePoolSize,则创建新线程来处理请求,即使其他辅助线程是空闲的。 if (workerCountOf(c) < corePoolSize) { if (addWorker(command, true)) return; c = ctl.get(); } //如果线程池中运行的线程数量>=corePoolSize,且线程池处于RUNNING状态,且把提交的任务成功放入阻塞队列中,就再次检查线程池的状态, // 1.如果线程池不是RUNNING状态,且成功从阻塞队列中删除任务,则该任务由当前 RejectedExecutionHandler 处理。 // 2.否则如果线程池中运行的线程数量为0,则通过addWorker(null, false)尝试新建一个线程,新建线程对应的任务为null。 if (isRunning(c) && workQueue.offer(command)) { int recheck = ctl.get(); if (!isRunning(recheck) && remove(command)) reject(command); else if (workerCountOf(recheck) == 0) addWorker(null, false); } // 如果以上两种case不成立,即没能将任务成功放入阻塞队列中,且addWoker新建线程失败,则该任务由当前 RejectedExecutionHandler 处理。 else if (!addWorker(command, false)) reject(command); }
- task:是具体的线程执行任务,线程在追加线程池的时候没有进行启动;
- worker:任务的执行需要worker来支持的,可以运行的worker受到“corePoolSize”限制;
- reject:如果现在线程池已经满了或者关闭了,那么就会出现拒绝新线程加入的可能性。
(2) ExecutorService: 增加Executor的行为,是Executor实现类的最直接接口。
(3) Executors: 提供了一系列工厂方法用于创先线程池,返回的线程池都实现了ExecutorService 接口。
创建线程池使用类:java.util.concurrent.Executors
Executors几个重要方法:
callable(Runnable task): 将 Runnable 的任务转化成 Callable 的任务
newSingleThreadExecutor(): 产生一个ExecutorService对象,这个对象只有一个线程可用来执行任务,若任务多于一个,任务将按先后顺序执行。
newCachedThreadPool(): 产生一个ExecutorService对象,这个对象带有一个线程池,线程池的大小会根据需要调整,线程执行完任务后返回线程池,供执行下一次任务使用。
//创建无大小限制的线程池 public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());
}
newFixedThreadPool(int poolSize): 产生一个ExecutorService对象,这个对象带有一个大小为 poolSize 的线程池,若任务数量大于 poolSize ,任务会被放在一个 queue 里顺序执行。
//创建固定大小的线程池 public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
}
newSingleThreadScheduledExecutor(): 产生一个ScheduledExecutorService对象,这个对象的线程池大小为 1 ,若任务多于一个,任务将按先后顺序执行。
//创建单线程池 public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()));
}
newScheduledThreadPool(int poolSize): 产生一个ScheduledExecutorService对象,这个对象的线程池大小为 poolSize ,若任务数量大于 poolSize ,任务会在一个 queue 里等待执行。
//创建定时调度池 public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
(4) ThreadPoolExecutor:线程池的具体实现类,一般用的各种线程池都是基于这个类实现的。
线程池可以解决两个不同问题:由于减少了每个任务的调用开销,在执行大量的异步任务时,它通常能够提供更好的性能,并且还可以提供绑定和管理资源(包括执行集合任务时使用的线程)的方法。每个 ThreadPoolExecutor还维护着一些基本的统计数据,如完成的任务数。
线程池做的其实可以看得很简单,其实就是把你提交的任务(task)进行调度管理运行,但这个调度的过程以及其中的状态控制是比较复杂的。
构造方法如下:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
if (corePoolSize < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize || keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null) throw new NullPointerException();
this.corePoolSize = corePoolSize; this.maximumPoolSize = maximumPoolSize; this.workQueue = workQueue; this.keepAliveTime = unit.toNanos(keepAliveTime); this.threadFactory = threadFactory; this.handler = handler; }
[1]corePoolSize:线程池的核心线程数,线程池中运行的线程数也永远不会超过 corePoolSize 个,默认情况下可以一直存活。可以通过设置allowCoreThreadTimeOut为True,此时 核心线程数就是0,此时keepAliveTime控制所有线程的超时时间。
核心线程会一直存活,及时没有任务需要执行,当线程数小于核心线程数时
即使有线程空闲,线程池也会优先创建新线程处理
设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭
[2]maximumPoolSize:线程池允许的最大线程数;
线程池最大线程数(当workQueue都放不下时,启动新线程)
当线程数>=corePoolSize,且任务队列已满时。线程池会创建新线程来处理任务
当线程数=maxPoolSize,且任务队列已满时,线程池会拒绝处理任务而抛出异常(饱和策略怎么处理)
[3]keepAliveTime: 指的是空闲线程结束的超时时间;
超出corePoolSize数量的线程的保留时间,如果allowCoreThreadTimeout=true,则会直到线程数量=0
jdk中的解释是:当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间。
有点拗口,其实这个不难理解,在使用了“池”的应用中,大多都有类似的参数需要配置。比如数据库连接池,DBCP中的maxIdle,minIdle参数。
什么意思?接着上面的解释,后来向老板派来的工人始终是“借来的”,俗话说“有借就有还”,但这里的问题就是什么时候还了,如果借来的工人刚完成一个任务就还回去,后来发现任务还有,那岂不是又要去借?这一来一往,老板肯定头也大死了。
合理的策略:既然借了,那就多借一会儿。直到“某一段”时间后,发现再也用不到这些工人时,便可以还回去了。这里的某一段时间便是keepAliveTime的含义,TimeUnit为keepAliveTime值的度量。
[4]unit :是一个枚举,表示 keepAliveTime 的单位;
TimeUnit.DAYS; //天 TimeUnit.HOURS; //小时 TimeUnit.MINUTES; //分钟 TimeUnit.SECONDS; //秒 TimeUnit.MILLISECONDS; //毫秒 TimeUnit.MICROSECONDS; //微妙 TimeUnit.NANOSECONDS; //纳秒
[5]workQueue:表示存放任务的BlockingQueue<Runnable队列。阻塞队列(任务队列容量),当线程数达到核心线程数时,新任务会放在队列中排队等待执行
ArrayBlockingQueue:构造函数一定要传大小
LinkedBlockingQueue:构造函数不传大小会默认为Integer.MAX_VALUE ,当大量请求任务时,容易造成 内存耗尽
SynchronousQueue:同步队列,一个没有存储空间的阻塞队列 ,将任务同步交付给工作线程
PriorityBlockingQueue : 优先队列
BlockingQueue:阻塞队列(BlockingQueue)是java.util.concurrent下的主要用来控制线程同步的工具。如果BlockQueue是空的,从BlockingQueue取东西的操作将会被阻断进入等待状态,直到BlockingQueue进了东西才会被唤醒。同样,如果BlockingQueue是满的,任何试图往里存东西的操作也会被阻断进入等待状态,直到BlockingQueue里有空间才会被唤醒继续操作。
阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。具体的实现类有LinkedBlockingQueue,ArrayBlockingQueued等。一般其内部的都是通过Lock和Condition(显示锁(Lock)及Condition的学习与使用)来实现阻塞和唤醒。
queue上的三种类型。
排队有三种通用策略:
直接提交:工作队列默认选项是SynchronousQueue,它将任务直接提交给线程而不保存它们。在此,如果不存在可用于立即运行任务的线程,则驶入把任务加入到队列将失败,因此将会构造一个新的线程。此策略可以避免在处理可能具有内部依赖性请求集时出现锁。直接提交通常要求无界maximumPoolSize以避免拒绝新提交的任务。当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程有增长的可能性。
无界队列:使用无界队列(例如,不具有预定义容量的 LinkedBlockingQueue)将导致在所有 corePoolSize 线程都忙时新任务在队列中等待。这样,创建的线程就不会超过 corePoolSize。(因此,maximumPoolSize的值也就无效了。)当每个任务完全独立于其他任务,即任务执行互不影响时,适合于使用无界队列;例如,在 Web页服务器中。这种排队可用于处理瞬态突发请求,当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。
有界队列。当使用有限的 maximumPoolSizes时,有界队列(如 ArrayBlockingQueue)有助于防止资源耗尽,但是可能较难调整和控制。队列大小和最大池大小可能需要相互折衷:使用大型队列和小型池可以最大限度地降低 CPU 使用率、操作系统资源和上下文切换开销,但是可能导致人工降低吞吐量。如果任务频繁阻塞(例如,如果它们是 I/O边界),则系统可能为超过您许可的更多线程安排时间。使用小型队列通常要求较大的池大小,CPU使用率较高,但是可能遇到不可接受的调度开销,这样也会降低吞吐量。
[6]threadFactory:线程工厂,主要用来创建线程
[7]rejectedExecutionHandler :任务拒绝处理器
两种情况会拒绝处理任务
- 当线程数已经达到maxPoolSize,切队列已满,会拒绝新任务
- 当线程池被调用shutdown()后,会等待线程池里的任务执行完毕,再shutdown。如果在调用shutdown()和线程池真正shutdown之间提交任务,会拒绝新任务
线程池会调用rejectedExecutionHandler来处理这个任务。如果没有设置默认是AbortPolicy,会抛出异常,ThreadPoolExecutor类有几个内部实现类来处理这类情况
- AbortPolicy(默认):直接抛弃,抛运行时异常
- CallerRunsPolicy:用调用者的线程执行任务
- DiscardOldestPolicy:抛弃队列中最久的任务
- DiscardPolicy:抛弃当前任务
实现RejectedExecutionHandler接口,可自定义处理器
另一种情况便是,即使向老板借了工人,但是任务还是继续过来,还是忙不过来,这时整个队伍只好拒绝接受了。
RejectedExecutionHandler接口提供了对于拒绝任务的处理的自定方法的机会。在ThreadPoolExecutor中已经默认包含了4中策略,因为源码非常简单,这里直接贴出来。
CallerRunsPolicy:线程调用运行该任务的 execute 本身。此策略提供简单的反馈控制机制,能够减缓新任务的提交速度。
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
AbortPolicy:处理程序遭到拒绝将抛出运行时RejectedExecutionException
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException();
}
DiscardPolicy:不能执行的任务将被删除
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}
这种策略和AbortPolicy几乎一样,也是丢弃任务,只不过他不抛出异常。
DiscardOldestPolicy:如果执行程序尚未关闭,则位于工作队列头部的任务将被删除,然后重试执行程序(如果再次失败,则重复此过程)
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll();
e.execute(r);
}
}
该策略就稍微复杂一些,在pool没有关闭的前提下首先丢掉缓存在队列中的最早的任务,然后重新尝试运行该任务。这个策略需要适当小心。
设想:如果其他线程都还在运行,那么新来任务踢掉旧任务,缓存在queue中,再来一个任务又会踢掉queue中最老任务。
线程池的拒绝策略
池子有对象池如commons pool的GenericObjectPool(通用对象池技术)也有java里面的线程池ThreadPoolExecutor,但java里面的线程池引入了一个叫拒绝执行的策略模式,感觉比GenericObjectPool好一点,意思也就是说当池子满的时候该如何执行还在不断往里面添加的一些任务。
像GenericObjectPool只提供了,继续等待和直接返回空的策略。而ThreadPoolExecutor则提供了一个接口,并内置了4中实现策略供用户分场景使用。
ThreadPoolExecutor.execute(Runnable command)提供了提交任务的入口,此方法会自动判断如果池子满了的话,则会调用拒绝策略来执行此任务,接口为RejectedExecutionHandler,内置的4中策略分别为AbortPolicy、DiscardPolicy、DiscardOldestPolicy、CallerRunsPolicy。
图5 拒绝策略关系图
AbortPolicy
为java线程池默认的阻塞策略,不执行此任务,而且直接抛出一个运行时异常,切记ThreadPoolExecutor.execute需要try catch,否则程序会直接退出。
DiscardPolicy
直接抛弃,任务不执行,空方法
DiscardOldestPolicy
从队列里面抛弃head的一个任务,并再次execute 此task。
CallerRunsPolicy
在调用execute的线程里面执行此command,会阻塞入口。
用户自定义拒绝策略
实现RejectedExecutionHandler,并自己定义策略模式。
再次需要注意的是,ThreadPoolExecutor.submit() 函数,此方法内部调用的execute方法,并把execute执行完后的结果给返回,但如果任务并没有执行的话(被拒绝了),则submit返回的future.get()会一直等到。
future 内部其实还是一个runnable,并把command给封装了下,当command执行完后,future会返回一个值。
handler:拒绝机制。
当线程池因为工作池已经饱和,准备拒绝任务时候。会调用RejectedExecutionHandler来拒绝该任务。Jdk提供了几种不同的RejectedExecutionHandler实现,每种实现都包含不同的饱和策略:AbortPolicy、CallerRunsPolicy、DiscardPolicy、DiscardOldestPolicy。
Abort是默认的饱和策略,该策略会抛出未检查的RejectedExecutionException。
CallerRuns实现一种调节机制,将任务回退到调用者,让调用者执行,从而降低了新任务的流量。webServer通过使用该策略使得在请求负载过高的情况下实现了性能的平缓降低。
Discard实现了会悄悄抛弃该任务,DiscardOldestPolicy会抛弃队列中抛弃下一个即将被执行的任务。如果是在优先队列里,DiscardOldestPolicy会抛弃优先级最高的任务。
ThreadLocalPool的池的大小设置,《Java并发编程实战》书中给了一个推荐的设置值。
Ncpu为CPU的数量,Ucpu为CPU的利用率,W/C为任务的等待时间 / 任务的计算时间。在这种情况下,一般线程池的最优大小:
N=Ncpu*Ucpu*(1+W/C)
线程池有五种运行状态
ThreadPoolExecutor中有一个ctl变量。ctl是一个32位的二级制数,其中高3位用于表示线程池的状态,低29位表示线程池中的活动线程。
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); private static final int COUNT_BITS = Integer.SIZE - 3; private static final int CAPACITY = (1 << COUNT_BITS) - 1; private static final int RUNNING = -1 << COUNT_BITS; private static final int SHUTDOWN = 0 << COUNT_BITS; private static final int STOP = 1 << COUNT_BITS; private static final int TIDYING = 2 << COUNT_BITS; private static final int TERMINATED = 3 << COUNT_BITS;
如上代码所示,线程池有五种状态。RUNNING、SHUTDOWN、STOP、TIDYING、TERMINNATED。幸好ThreadPoolExecutor的代码上有对应注释,看着这些注释能对ThreadPoolExecutor的状态作用和状态流转能有一个大致的了解。
RUNNING:在线程池创建的时候,线程池默认处于RUNNING状态。当线程池处于RUNNING状态的时候,任务队列可以接受任务,并且可以执行QUEUE中任务。
SHUTDOWN:不接受新任务,但是会继续执行QUEUE中的任务。
STOP:不接受新任务,也不执行QUEUE中的任务。
TIDYING:所有的任务都中止了,没有活动中的线程。当线程池进行该状态时候,会执行钩子方法terminated() 。
Executors的执行
当Executors创建完成了线程池之后可以返回“ExecutorService”接口对象,而这个对象里面有两个方法来接收线程的执行:
//接收Callable: public <T> Future<T> submit(Callable<T> task); //接收Runnable: public Future<?> submit(Runnable task);
范例:创建无限量线程池
package so.strong.mall.concurrent; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ExecutorDemo { public static void main(String[] args) { ExecutorService service = Executors.newCachedThreadPool(); //创建一个线程池 for (int i = 0; i < 5; i++) { service.submit(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()+"执行操作"); } }); } service.shutdown(); //线程池执行完毕后需要关闭 } } //无限量大小的线程池会根据内部线程的执行状况来进行线程对象个数的控制。
submit()方法是可以接收Callable接口对象的
package so.strong.mall.concurrent; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; public class ExecutorDemo { public static void main(String[] args) throws Exception { ExecutorService service = Executors.newCachedThreadPool(); for (int i = 0; i < 5; i++) { Future<?> future = service.submit(new Callable<Object>() { @Override public Object call() throws Exception { return Thread.currentThread().getName() + "执行操作"; } }); System.out.println(future.get()); } service.shutdown(); } }
Future线程模型设计的优势在于:可以进行线程数据的异步控制,但是在之前编写的过程严格来讲并不好,相当于启动了一个线程就获得了一个返回值,于是为了方便这些线程池中线程对象的管理,可以使用如下方法进行统一返回:
public interface ExecutorService extends Executor { public <T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException; }
范例:使用invokeAny()方法
package so.strong.mall.concurrent; import java.util.HashSet; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ExecutorDemo { public static void main(String[] args) throws Exception { Set<Callable<String>> tasks = new HashSet<>(); //所有任务 for (int i = 0; i < 10; i++) { final int temp = i; tasks.add(new Callable<String>() { @Override public String call() throws Exception { return Thread.currentThread().getName() + "执行任务,i=" + temp; } }); } ExecutorService service = Executors.newCachedThreadPool(); //创建一个线程池 String invokeAny = service.invokeAny(tasks); //执行任务 System.out.println("返回结果:" + invokeAny); service.shutdown(); } } //返回结果:pool-1-thread-2执行任务,i=4 //使用invokeAny()方法只会返回一个任务的执行操作
CompletionService线程池异步交互
package java.util.concurrent; public interface CompletionService<V> { Future<V> submit(Callable<V> task); Future<V> submit(Runnable task, V result); Future<V> take() throws InterruptedException; Future<V> poll(); Future<V> poll(long timeout, TimeUnit unit) throws InterruptedException; }
线程池异步交互:CompletionService
将生产新的异步任务与使用已完成任务的结果分离开来的服务。生产者submit()执行的任务,使用者take()已完成的任务,并按照完成任务的顺序处理它们的结果。
CompletionService依赖于一个单独的Executor来实际执行任务,在这种情况下,CompletionService只管理一个内部完成队列,在CompletionService接口里面提供有如下两个方法:
设置Callable:
public Future<V> submit(Callable<V> task);
设置Runnable:
public Future<V> submit(Runnable task, V result);
CompletionService是一个接口,如果要想使用这个接口可以采用ExecutorCompletionService这个子类
public class ExecutorCompletionService<V> implements CompletionService<V>
ExecutorCompletionService的构造方法:
public ExecutorCompletionService(Executor executor) { if (executor == null) throw new NullPointerException(); this.executor = executor; this.aes = (executor instanceof AbstractExecutorService) ? (AbstractExecutorService) executor : null; this.completionQueue = new LinkedBlockingQueue<Future<V>>(); }
CompletionService来控制所有线程池的操作以及数据返回,则应该使用这个类来进行线程池的提交处理。
提交线程
Future<V> submit(Callable<V> task);
获取返回内容
Future<V> take() throws InterruptedException;
范例:使用CompletionService工具类
package so.strong.mall.concurrent; import java.util.concurrent.*; public class ExecutorDemo { public static void main(String[] args) throws Exception { ExecutorService service = Executors.newCachedThreadPool(); CompletionService<String> completions = new ExecutorCompletionService<>(service); for (int i = 0; i < 5; i++) { final int temp = i; completions.submit(new Callable<String>() { @Override public String call() throws Exception { return Thread.currentThread().getName() + "- i =" + temp; } }); } for (int i = 0; i < 5; i++) { System.out.println(completions.take().get()); } service.shutdown(); } }
CompletionService操作接口的主要目的是可以去隐藏ExecutorService接口执行线程池的处理,不在需要关心novkeAny(), invokeAll()的执行方法了。
创建一个定时调度池,这个调度池主要是以时间间隔调度为主。如果要创建调度池则使用ScheduledExecutorService接口完成,该接口之中包含有如下的两个方法:
延迟启动
public ScheduledFuture<?> schedule(Runnable command,long delay, TimeUnit unit);
间隔调度
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit);
范例:创建调度池
package so.strong.mall.concurrent; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; public class ExecutorDemo { public static void main(String[] args) { ScheduledExecutorService service = Executors.newScheduledThreadPool(1); for (int i = 0; i < 5; i++) { service.schedule(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()+"执行操作"); } },2, TimeUnit.SECONDS); } service.shutdown(); } }
在ExecutorService接口里面的确提供有接收Runnable接口对象的方法,但是这个方法为了统一使用的是submit()。submit()重载了许多次,可以接收Runnable:
public Future<?> submit(Runnale task)
线程池的创建和使用
生成线程池采用了工具类Executors的静态方法,以下是几种常见的线程池。
SingleThreadExecutor:单个后台线程 (其缓冲队列是无界的)
public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService ( new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); }
创建一个单线程的线程池。这个线程池只有一个核心线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
FixedThreadPool:只有核心线程的线程池,大小固定 (其缓冲队列是无界的) 。
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); }
创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
CachedThreadPool:无界线程池,可以进行自动线程回收。
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0,Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); }
如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。SynchronousQueue是一个是缓冲区为1的阻塞队列。
ScheduledThreadPool:核心线程池固定,大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
public static ExecutorService newScheduledThreadPool(int corePoolSize) { return new ScheduledThreadPool(corePoolSize, Integer.MAX_VALUE, DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS, new DelayedWorkQueue()); }
创建一个周期性执行任务的线程池。如果闲置,非核心线程池会在DEFAULT_KEEPALIVEMILLIS时间内回收。
线程池最常用的提交任务的方法有两种:
execute:
ExecutorService.execute(Runnable runable);
submit:
FutureTask task = ExecutorService.submit(Runnable runnable);
FutureTask<T> task = ExecutorService.submit(Runnable runnable,T Result); FutureTask<T> task = ExecutorService.submit(Callable<T> callable);
submit(Callable callable)的实现,submit(Runnable runnable)同理。
public <T> Future<T> submit(Callable<T> task) { if (task == null) throw new NullPointerException(); FutureTask<T> ftask = newTaskFor(task); execute(ftask); return ftask; }
可以看出submit开启的是有返回结果的任务,会返回一个FutureTask对象,这样就能通过get()方法得到结果。submit最终调用的也是execute(Runnable runable),submit只是将Callable对象或Runnable封装成一个FutureTask对象,因为FutureTask是个Runnable,所以可以在execute中执行。关于Callable对象和Runnable怎么封装成FutureTask对象,见Callable和Future、FutureTask的使用。
扩展ThreadPoolExecutor:
ThreadPoolExecutor提供了以下3个生命周期的钩子方法让子类扩展:
(1).beforeExecute:
任务执行前,线程会调用该方法,可以用来添加日志、监控或者信息收集统计。
若beforeExcute方法抛出了RuntimeException,线程的任务将不被执行,afterExecute方法也不会被调用。
(2).afterExecute:
任务执行结束后,线程会调用该方法,可以用来添加日志、监控或者信息收集统计。
无论任务正常返回或者抛出异常(抛出Error不能被调用),该方法都会被调用。
(3).terminate:
线程池完成关闭动作后调用,可以用来释放资源、发出通知、记录日志或者完成统计信息等。
一个扩展ThreadPoolExecutor的例子代码如下:
public class TimingThreadPool extends ThreadPoolExecutor{ private final ThreadLocal<Long> startTime = new ThreadLocal<Long>(); private final Logger log = Logger.getLogger(TimingThreadPool.class.getClassName()); private final AtomicLong numTasks = new AtomicLong(); private final AtomicLong totalTime = new AtomicLong(); protected void beforeExecute(Thread t, Runnable r){ super.beforeExecute(t, r); log.fine(String.format(“Thread %s: start %s”, t, r)); startTime.set(System.nanoTime()); } protected void afterExecute(Runnable r, Throwable t){ try{ long endTime = System.nanoTime(); long taskTime = endTime - startTime.get(); numTasks.incrementAndGet(); totalTime.addAndGet(taskTime); log.fine(String.format("Thread %s: end %s, time=%dns", t, r, taskTime)); }finally{ super.afterExecute(r, t); } } protected void terminated(){ try{ log.info(String.format("Terminated: avg time=%dns", totalTime.get() / numTasks.get())); }finally{ super.terminated(); } } }
总线程数 = 排队线程数 + 活动线程数 + 执行完成的线程数。
下面给出一个线程池使用示例,及教你获取线程池状态。
private static ExecutorService es = new ThreadPoolExecutor(50, 100, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(100000)); public static void main(String[] args) throws Exception { for (int i = 0; i < 100000; i++) { es.execute(() -> { System.out.print(1); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } }); } ThreadPoolExecutor tpe = ((ThreadPoolExecutor) es); while (true) { System.out.println(); int queueSize = tpe.getQueue().size(); System.out.println("当前排队线程数:" + queueSize); int activeCount = tpe.getActiveCount(); System.out.println("当前活动线程数:" + activeCount); long completedTaskCount = tpe.getCompletedTaskCount(); System.out.println("执行完成线程数:" + completedTaskCount); long taskCount = tpe.getTaskCount(); System.out.println("总线程数:" + taskCount); Thread.sleep(3000); } }
线程池提交了 100000 个任务,但同时只有 50 个线程在执行工作,我们每陋 3 秒来获取当前线程池的运行状态。
第一次程序输出:
当前排队线程数:99950 当前活动线程数:50 执行完成线程数:0 总线程数(排队线程数 + 活动线程数 + 执行完成线程数):100000
第二次程序输出:
当前排队线程数:99800 当前活动线程数:50 执行完成线程数:150 总线程数(排队线程数 + 活动线程数 + 执行完成线程数):100000
活动线程数和总线程数是不变的,排队中的线程数和执行完成的线程数不断在变化,直到所有任务执行完毕,最后输出:
当前排队线程数:0 当前活动线程数:0 执行完成线程数:100000 总线程数(排队线程数 + 活动线程数 + 执行完成线程数):100000
这样,你了解了这些 API 的使用方法,你想监控线程池的状态就非常方便了。
Java 8 增强的线程池
为了充分利用多CPU的优势、多核CPU的性能优势。可以考多个小任务,把小任务放到多个处理器核心上并行执行;当多个小任务执行完成之后,再将这些执行结果合并起来即可。Java 7提供了ForkJoinPool来支持这个功能。
ForkJoinPool是ExecutorService的实现类,因此是一种特殊的线程池。提供了如下两个常用的构造器
- ForkJoinPool(int parallelism):创建一个包含parallelism个并行线程的ForkJoinPool.
- ForkJoinPool():以Runtime.availableProssesors()方法的返回值作为paralelism参数来创建ForkJoinPool.
Java 8进一步拓展了ForkJoinPool的功能,Java 8增加了通用池功能。ForkJoinPool通过如下两个方法提供通用池功能。
- ForkJoinPool commonPool():该方法返回一个通用池,通用池的状态不会受shutdown()或shutdownNow()方法的影响。
- int getCommonPoolParallelism():该方法返回通用池的并行级别。
创建了通用池ForkJoinPool实例之后,就可调用ForkJoinPool的submit(ForkJoinTask task)或invoke(ForkJoinTask task)方法来执行指定任务了。其中,ForkJoinTask代表一个并行,合并的任务。
ForkJoinTask是一个抽象类,它还有两个抽象子类:RecursiveAction和recursiveTask。其中RecursiveAction代表没有返回值的任务,RecursiveTask代表有返回值的任务。
下面程序将一个大任务(打印0~500)的数值分成多个小任务,并将任务交给ForkJoinPool来执行。
package com.gdut.thread.threadPool; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.RecursiveAction; import java.util.concurrent.TimeUnit; class PrintTask extends RecursiveAction{ private static final int THRESHOLD = 50; private int start; private int end; public PrintTask(int start,int end) { this.start = start; this.end = end; } @Override protected void compute() { if(end-start<THRESHOLD){ for (int i = start; i <end ; i++) { System.out.println(Thread.currentThread().getName()+"的i值"+i); } }else{ //当end与start的差大于THRESHOLD时,即要打印的数超过50时,将大任务分成两个小任务 int middle = (end+start)/2; PrintTask left = new PrintTask(start,middle); PrintTask right = new PrintTask(middle,end); left.fork(); right.fork(); } } } public class ForkJoinPoolTest{ public static void main(String[] args) throws InterruptedException{ ForkJoinPool pool = new ForkJoinPool(); pool.submit(new PrintTask(0,500)); pool.awaitTermination(2, TimeUnit.SECONDS); pool.shutdown(); } }