java线程池使⽤实例6_Java并发编程:线程池的使⽤
Java并发编程:线程池的使⽤
在前⾯的⽂章中,我们使⽤线程的时候就去创建⼀个线程,这样实现起来⾮常简便,但是就会有⼀个问题:
如果并发的线程数量很多,并且每个线程都是执⾏⼀个时间很短的任务就结束了,这样频繁创建线程就会⼤⼤降低系统的效率,因为频繁创建线程和销毁线程需要时间。
那么有没有⼀种办法使得线程可以复⽤,就是执⾏完⼀个任务,并不被销毁,⽽是可以继续执⾏其他的任务?
在Java中可以通过线程池来达到这样的效果。今天我们就来详细讲解⼀下Java的线程池,⾸先我们从最核⼼的ThreadPoolExecutor类中的⽅法讲起,然后再讲述它的实现原理,接着给出了它的使⽤⽰例,最后讨论了⼀下如何合理配置线程池的⼤⼩。
以下是本⽂的⽬录⼤纲:
⼀.Java中的ThreadPoolExecutor类
⼆.深⼊剖析线程池实现原理
三.使⽤⽰例
四.如何合理配置线程池的⼤⼩
若有不正之处请多多谅解,并欢迎批评指正。
请尊重作者劳动成果,转载请标明原⽂链接:
⼀.Java中的ThreadPoolExecutor类
urrent.ThreadPoolExecutor类是线程池中最核⼼的⼀个类,因此如果要透彻地了解Java中的线程池,必须先了解这个类。下⾯我们来看⼀下ThreadPoolExecutor类的具体实现源码。
在ThreadPoolExecutor类中提供了四个构造⽅法:
从上⾯的代码可以得知,ThreadPoolExecutor继承了AbstractExecutorService类,并提供了四个构造器,事实上,通过观察每个构造器的源码具体实现,发现前⾯三个构造器都是调⽤的第四个构造器进⾏的初始化⼯作。
下⾯解释下⼀下构造器中各个参数的含义:
corePoolSize:核⼼池的⼤⼩,这个参数跟后⾯讲述的线程池的实现原理有⾮常⼤的关系。在创建了线程池后,默认情况下,线程池中并没有任何线程,⽽是等待有任务到来才创建线程去执⾏任务,除⾮调⽤了prestartAllCoreThreads()或者prestartCoreThread()⽅法,从这2个⽅法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者⼀个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建⼀个线程去执⾏任务,当线程池中的线程数⽬达到corePoolSize后,就会把到达的任务放到缓存队列当中;
maximumPoolSize:线程池最⼤线程数,这个参数也是⼀个⾮常重要的参数,它表⽰在线程池中最多能创建多少个线程;
keepAliveTime:表⽰线程没有任务执⾏时最多保持多久时间会终⽌。默认情况下,只有当线程池中的线程数⼤于corePoolSize
时,keepAliveTime才会起作⽤,直到线程池中的线程数不⼤于corePoolSize,即当线程池中的线程数⼤于corePoolSize时,如果⼀个线程空闲的时间达到keepAliveTime,则会终⽌,直到线程池中的线程数不超过corePoolSize。但是如果调⽤了
allowCoreThreadTimeOut(boolean)⽅法,在线程池中的线程数不⼤于corePoolSize时,keepAliveTime参数也会起作⽤,直到线程池中的线程数为0;
unit:参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性:
TimeUnit.DAYS; //天
TimeUnit.HOURS; //⼩时
TimeUnit.MINUTES; //分钟
TimeUnit.SECONDS; //秒
TimeUnit.MILLISECONDS; //毫秒
TimeUnit.MICROSECONDS; //微妙
TimeUnit.NANOSECONDS; //纳秒
workQueue:⼀个阻塞队列,⽤来存储等待执⾏的任务,这个参数的选择也很重要,会对线程池的运⾏过程产⽣重⼤影响,⼀般来说,这⾥的阻塞队列有以下⼏种选择:
ArrayBlockingQueue;
LinkedBlockingQueue;
SynchronousQueue;
ArrayBlockingQueue和PriorityBlockingQueue使⽤较少,⼀般使⽤LinkedBlockingQueue和Synchronous。线程池的排队策略与BlockingQueue有关。
threadFactory:线程⼯⼚,主要⽤来创建线程;
handler:表⽰当拒绝处理任务时的策略,有以下四种取值:
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前⾯的任务,然后重新尝试执⾏任务(重复此过
程)
ThreadPoolExecutor.CallerRunsPolicy:由调⽤线程处理该任务
具体参数的配置与线程池的关系将在下⼀节讲述。
从上⾯给出的ThreadPoolExecutor类的代码可以知道,ThreadPoolExecutor继承了AbstractExecutorService,我们来看⼀下AbstractExecutorService的实现:
AbstractExecutorService是⼀个抽象类,它实现了ExecutorService接⼝。
我们接着看ExecutorService接⼝的实现:
⽽ExecutorService⼜是继承了Executor接⼝,我们看⼀下Executor接⼝的实现:
到这⾥,⼤家应该明⽩了ThreadPoolExecutor、AbstractExecutorService、ExecutorService和Executor⼏个之间的关系了。
Executor是⼀个顶层接⼝,在它⾥⾯只声明了⼀个⽅法execute(Runnable),返回值为void,参数为Runnable类型,从字⾯意思可以理解,就是⽤来执⾏传进去的任务的;
然后ExecutorService接⼝继承了Executor接⼝,并声明了⼀些⽅法:submit、invokeAll、invokeAny以及shutDown等;
抽象类AbstractExecutorService实现了ExecutorService接⼝,基本实现了ExecutorService中声明的所有⽅法;
然后ThreadPoolExecutor继承了类AbstractExecutorService。
在ThreadPoolExecutor类中有⼏个⾮常重要的⽅法:
execute()⽅法实际上是Executor中声明的⽅法,在ThreadPoolExecutor进⾏了具体的实现,这个⽅法是ThreadPoolExecutor的核⼼⽅法,通过这个⽅法可以向线程池提交⼀个任务,交由线程池去执⾏。
submit()⽅法是在ExecutorService中声明的⽅法,在AbstractExecutorService就已经有了具体的实现,在ThreadPoolExecutor中并没有对其进⾏重写,这个⽅法也是⽤来向线程池提交任务的,但是它和execute()⽅法不同,它能够返回任务执⾏的结果,去看submit()⽅法的实现,会发现它实际上还是调⽤的execute()⽅法,只不过它利⽤了Future来获取任务执⾏结果(Future相关内容将在下⼀篇讲述)。
shutdown()和shutdownNow()是⽤来关闭线程池的。
还有很多其他的⽅法:
⽐如:getQueue() 、getPoolSize() 、getActiveCount()、getCompletedTaskCount()等获取与线程池相关属性的⽅法,有兴趣的朋友可以⾃⾏查阅API。
⼆.深⼊剖析线程池实现原理
在上⼀节我们从宏观上介绍了ThreadPoolExecutor,下⾯我们来深⼊解析⼀下线程池的具体实现原理,将从下⾯⼏个⽅⾯讲解:
1.线程池状态
2.任务的执⾏
3.线程池中的线程初始化
4.任务缓存队列及排队策略
5.任务拒绝策略java线程池创建的四种
6.线程池的关闭
7.线程池容量的动态调整
1.线程池状态
在ThreadPoolExecutor中定义了⼀个volatile变量,另外定义了⼏个static final变量表⽰线程池的各个状态:
runState表⽰当前线程池的状态,它是⼀个volatile变量⽤来保证线程之间的可见性;
下⾯的⼏个static final变量表⽰runState可能的⼏个取值。
当创建线程池后,初始时,线程池处于RUNNING状态;
如果调⽤了shutdown()⽅法,则线程池处于SHUTDOWN状态,此时线程池不能够接受新的任务,它会等待所有任务执⾏完毕;
如果调⽤了shutdownNow()⽅法,则线程池处于STOP状态,此时线程池不能接受新的任务,并且会去尝试终⽌正在执⾏的任务;
当线程池处于SHUTDOWN或STOP状态,并且所有⼯作线程已经销毁,任务缓存队列已经清空或执⾏结束后,线程池被设置为TERMINATED状态。
2.任务的执⾏
在了解将任务提交给线程池到任务执⾏完毕整个过程之前,我们先来看⼀下ThreadPoolExecutor类中其他的⼀些⽐较重要成员变量:
每个变量的作⽤都已经标明出来了,这⾥要重点解释⼀下corePoolSize、maximumPoolSize、largestPoolSize三个变量。
corePoolSize在很多地⽅被翻译成核⼼池⼤⼩,其实我的理解这个就是线程池的⼤⼩。举个简单的例⼦:
假如有⼀个⼯⼚,⼯⼚⾥⾯有10个⼯⼈,每个⼯⼈同时只能做⼀件任务。
因此只要当10个⼯⼈中有⼯⼈是空闲的,来了任务就分配给空闲的⼯⼈做;
当10个⼯⼈都有任务在做时,如果还来了任务,就把任务进⾏排队等待;
如果说新任务数⽬增长的速度远远⼤于⼯⼈做任务的速度,那么此时⼯⼚主管可能会想补救措施,⽐如重新招4个临时⼯⼈进来;
然后就将任务也分配给这4个临时⼯⼈做;
如果说着14个⼯⼈做任务的速度还是不够,此时⼯⼚主管可能就要考虑不再接收新的任务或者抛弃前⾯的⼀些任务了。
当这14个⼯⼈当中有⼈空闲时,⽽新任务增长的速度⼜⽐较缓慢,⼯⼚主管可能就考虑辞掉4个临时⼯了,只保持原来的10个⼯⼈,毕竟请额外的⼯⼈是要花钱的。
这个例⼦中的corePoolSize就是10,⽽maximumPoolSize就是14(10+4)。
也就是说corePoolSize就是线程池⼤⼩,maximumPoolSize在我看来是线程池的⼀种补救措施,即任务量突然过⼤时的⼀种补救措施。
不过为了⽅便理解,在本⽂后⾯还是将corePoolSize翻译成核⼼池⼤⼩。
largestPoolSize只是⼀个⽤来起记录作⽤的变量,⽤来记录线程池中曾经有过的最⼤线程数⽬,跟线程池的容量没有任何关系。
下⾯我们进⼊正题,看⼀下任务从提交到最终执⾏完毕经历了哪些过程。
在ThreadPoolExecutor类中,最核⼼的任务提交⽅法是execute()⽅法,虽然通过submit也可以提交任务,但是实际上submit⽅法⾥⾯最终调⽤的还是execute()⽅法,所以我们只需要研究execute()⽅法的实现原理即可:
上⾯的代码可能看起来不是那么容易理解,下⾯我们⼀句⼀句解释:
⾸先,判断提交的任务command是否为null,若是null,则抛出空指针异常;
接着是这句,这句要好好理解⼀下:
由于是或条件运算符,所以先计算前半部分的值,如果线程池中当前线程数不⼩于核⼼池⼤⼩,那么就会直接进⼊下⾯的if语句块了。
如果线程池中当前线程数⼩于核⼼池⼤⼩,则接着执⾏后半部分,也就是执⾏
如果执⾏完addIfUnderCorePoolSize这个⽅法返回false,则继续执⾏下⾯的if语句块,否则整个⽅法就直接执⾏完毕了。
如果执⾏完addIfUnderCorePoolSize这个⽅法返回false,然后接着判断:
如果当前线程池处于RUNNING状态,则将任务放⼊任务缓存队列;如果当前线程池不处于RUNNING状态或者任务放⼊缓存队列失败,则执⾏:
如果执⾏addIfUnderMaximumPoolSize⽅法失败,则执⾏reject()⽅法进⾏任务拒绝处理。
回到前⾯:
这句的执⾏,如果说当前线程池处于RUNNING状态且将任务放⼊任务缓存队列成功,则继续进⾏判断:
这句判断是为了防⽌在将此任务添加进任务缓存队列的同时其他线程突然调⽤shutdown或者shutdownNow⽅法关闭了线程池的⼀种应急措施。如果是这样就执⾏:
进⾏应急处理,从名字可以看出是保证 添加到任务缓存队列中的任务得到处理。
我们接着看2个关键⽅法的实现:addIfUnderCorePoolSize和addIfUnderMaximumPoolSize:
这个是addIfUnderCorePoolSize⽅法的具体实现,从名字可以看出它的意图就是当低于核⼼吃⼤⼩时执⾏的⽅法。下⾯看其具体实现,⾸先获取到锁,因为这地⽅涉及到线程池状态的变化,先通过if语句判断当前线程池中的线程数⽬是否⼩于核⼼池⼤⼩,有朋友也许会有疑问:前⾯在execute()⽅法中不是已经判断过了吗,只有线程池当前线程数⽬⼩于核⼼池⼤⼩才会执⾏addIfUnderCorePoolSize⽅法的,为何这地⽅还要继续判断?原因很简单,前⾯的判断过程中并没有加锁,因此可能在execute⽅法判断的时候poolSize⼩于corePoolSize,⽽判断完之后,在其他线程中⼜向线程池提交了任务,就可能导致poolSize不⼩于corePoolSize了,所以需要在这个地⽅继续判断。然后接着判断线程池的状态
是否为RUNNING,原因也很简单,因为有可能在其他线程中调⽤了shutdown或者shutdownNow ⽅法。然后就是执⾏
这个⽅法也⾮常关键,传进去的参数为提交的任务,返回值为Thread类型。然后接着在下⾯判断t是否为空,为空则表明创建线程失败(即poolSize>=corePoolSize或者runState不等于RUNNING),否则调⽤t.start()⽅法启动线程。
我们来看⼀下addThread⽅法的实现:
在addThread⽅法中,⾸先⽤提交的任务创建了⼀个Worker对象,然后调⽤线程⼯⼚threadFactory创建了⼀个新的线程t,然后将线程t 的引⽤赋值给了Worker对象的成员变量thread,接着通过workers.add(w)将Worker对象添加到⼯作集当中。
下⾯我们看⼀下Worker类的实现:
它实际上实现了Runnable接⼝,因此上⾯的Thread t = wThread(w);效果跟下⾯这句的效果基本⼀样:
相当于传进去了⼀个Runnable任务,在线程t中执⾏这个Runnable。
既然Worker实现了Runnable接⼝,那么⾃然最核⼼的⽅法便是run()⽅法了:
从run⽅法的实现可以看出,它⾸先执⾏的是通过构造器传进来的任务firstTask,在调⽤runTask()执⾏完firstTask之后,在while循环⾥⾯不断通过getTask()去取新的任务来执⾏,那么去哪⾥取呢?⾃然是从任务缓存队列⾥⾯去取,getTask是ThreadPoolExecutor类中的⽅法,并不是Worker类中的⽅法,下⾯是getTask⽅法的实现:
在getTask中,先判断当前线程池状态,如果runState⼤于SHUTDOWN(即为STOP或者TERMINATED),则直接返回null。
如果runState为SHUTDOWN或者RUNNING,则从任务缓存队列取任务。
如果当前线程池的线程数⼤于核⼼池⼤⼩corePoolSize或者允许为核⼼池中的线程设置空闲存活时间,则调⽤poll(time,timeUnit)来取任务,这个⽅法会等待⼀定的时间,如果取不到任务就返回null。
然后判断取到的任务r是否为null,为null则通过调⽤workerCanExit()⽅法来判断当前worker是否可以退出,我们看⼀下workerCanExit()的实现:
也就是说如果线程池处于STOP状态、或者任务队列已为空或者允许为核⼼池线程设置空闲存活时间并且线程数⼤于1时,允许worker退出。如果允许worker退出,则调⽤interruptIdleWorkers()中断处于空闲状态的worker,我们看⼀下interruptIdleWorkers()的实现:
从实现可以看出,它实际上调⽤的是worker的interruptIfIdle()⽅法,在worker的interruptIfIdle()⽅法中:
这⾥有⼀个⾮常巧妙的设计⽅式,假如我们来设计线程池,可能会有⼀个任务分派线程,当发现有线程空闲时,就从任务缓存队列中取⼀个任务交给空闲线程执⾏。但是在这⾥,并没有采⽤这样的⽅式,因为这样会要额外地对任务分派线程进⾏管理,⽆形地会增加难度和复杂度,这⾥直接让执⾏完任务的线程去任务缓存队列⾥⾯取任务来执⾏。
我们再看addIfUnderMaximumPoolSize⽅法的实现,这个⽅法的实现思想和addIfUnderCorePoolSize⽅法的实现思想⾮常相似,唯⼀的区别在于addIfUnderMaximumPoolSize⽅法是在线程池中的线程数达到了核⼼池⼤⼩并且往任务队列中添加任务失败的情况下执⾏的:
看到没有,其实它和addIfUnderCorePoolSize⽅法的实现基本⼀模⼀样,只是if语句判断条件中的poolSize < maximumPoolSize不同⽽已。
到这⾥,⼤部分朋友应该对任务提交给线程池之后到被执⾏的整个过程有了⼀个基本的了解,下⾯总结⼀下:
1)⾸先,要清楚corePoolSize和maximumPoolSize的含义;
2)其次,要知道Worker是⽤来起到什么作⽤的;
3)要知道任务提交给线程池之后的处理策略,这⾥总结⼀下主要有4点:
如果当前线程池中的线程数⽬⼩于corePoolSize,则每来⼀个任务,就会创建⼀个线程去执⾏这个任务;
如果当前线程池中的线程数⽬>=corePoolSize,则每来⼀个任务,会尝试将其添加到任务缓存队列当中,若添加成功,则该任务会等待空闲线程将其取出去执⾏;若添加失败(⼀般来说是任务缓存队列已满),则会尝试创建新的线程去执⾏这个任务;
如果当前线程池中的线程数⽬达到maximumPoolSize,则会采取任务拒绝策略进⾏处理;
如果线程池中的线程数量⼤于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终⽌,直⾄线程池中的线程数⽬不⼤于corePoolSize;如果允许为核⼼池中的线程设置存活时间,那么核⼼池中的线程空闲时间超过keepAliveTime,线程也会被终⽌。
3.线程池中的线程初始化
默认情况下,创建线程池之后,线程池中是没有线程的,需要提交任务之后才会创建线程。
在实际中如果需要线程池创建之后⽴即创建线程,可以通过以下两个⽅法办到:
prestartCoreThread():初始化⼀个核⼼线程;
prestartAllCoreThreads():初始化所有核⼼线程
下⾯是这2个⽅法的实现:
注意上⾯传进去的参数是null,根据第2⼩节的分析可知如果传进去的参数为null,则最后执⾏线程会阻塞在getTask⽅法中的
即等待任务队列中有任务。
4.任务缓存队列及排队策略
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论