首页 > 技术文章 > Executor以及线程池

dhh-blog 2017-04-09 10:07 原文

在应用程序中,总是会出现大量的任务,包括相同类型的和不同类型的。要快速处理这些任务,常见方法就是利用多线程,但是也不可能为每个任务都创建一个线程,这样内存也不够,并且线程的创建销毁开销很大。最好是少量线程处理大量任务,实现线程的复用,Executor干的就是这事。程序只需要把任务提交给Executor,由Executor来确定怎么来执行这个任务,即执行策略。Executor的关闭很重要,如果Executor如果没有关闭,那JVM将无法结束。关闭方法有shutdown,shutdownNow,shutdown方法执行平缓关闭,若该任务没有提交则拒绝提交,若该任务已经提交但是没有执行,将等待它执行完成,若该任务已经执行将等待它执行完成。shutdownNow是粗暴关闭,取消现在执行的任务,拒绝处理新任务,不再启动线程池队列中已提交的任务。当所有已经提交到Executor的任务都执行完成时,Executor将进入terminatede终止状态。

线程池的底层原理:数据结构:线程池管理器(ThreadpoolFactory),worker,任务队列Blockqueue;线程池的调度:新提交一个任务时,所有看下现有的线程数量是不是达到了corepoolSize,如果没有将新建一个线程thread,该线程将执行该任务,当该任务执行完成时,它会去任务队列中取出任务然后执行任务。若当前线程的数量大于corepoolSize小于maxpoolSize,则看下任务队列是否已经满了,若满了则新建线程thread,若没有满则将任务丢到任务队列中。

任务的取消和关闭

建设一个任务执行到一半,程序想取消这个任务,那怎么办呢?最好的方法是由每个任务自己来决定如何取消自己,这将保持数据的一致性,不会乱。常见的方法是轮询一个取消标志位,当该标志位被设立了,则执行自己的取消策略。这种方式会有个弊端,当执行该任务的线程由于某种原因被阻塞掉,那它将看不到标志位的设立,这个时候将加入中断Interrupted。当调用Thread.interrupted()时,线程的中断标志位会被设立,阻塞方法会检查中断标志位,将发现中断标志位为true时,将结束阻塞,重置中断位,提前返回并抛出中断异常。中断处理策略:抛出异常(我不想管了,谁爱管谁管),捕获异常(管了就不能无作为),恢复中断位(我想管,我告诉别人我是要中断的)。说了这么多如何管理任务的生命周期,最后发现Future已经提供了任务的周期的管理抽象,尴尬。对于不可中断的阻塞方法(同步io,Selector.select()方法,获取内置锁)通过中断不能达到取消任务的效果,只能特殊处理,比如说关闭socket.close(),抛出中断异常。当一个线程执行任务时,任务抛出一个未受检异常。jvm将捕获到异常,并终止线程,但是对于Executor这样的服务来说,终止一个线程可能会使它产生影响,所以它捕获异常并新增一条工作者线程(具体是新增还是不管看当前的线程数)。虽然这看起来好像没什么问题,但是这对于程序来说却显的莫名其妙,一个任务无声无息就取消了。针对这种情况提供了一个UncaughtExceptionHandler异常捕获器,线程池可以为每个线程配一个异常捕获器(通过线程池管理器实现)。

线程池的使用

任务的种类比较复杂,为了更好的利用线程池的优势,对于线程池需要显示配置,选择Executors.newFixThreadpool,Executors.newCacheThreadpool,Executors.newSingleThreadpool,Executors.newScheduleThreadExecutor创建不同的线程池。选择哪种线程池:singleThreadpool线程池可以更好的同步,当多个任务同时操作一个资源,即使该资源不是同步的,使用singleThreadpool线程池能保证当前任务对状态的修改能对下一个任务可见。FixThreadpool用于限制任务的数量以保证资源过程问题(网络操作常见),cacheThreadpool适用于长时间任务和短时间任务并行的场景,依赖性任务(会出现线程饥饿)。线程池大小的选择,计算型密集的任务选择跟cpu数,IO密集型的根据公式计算(偷个懒,不写了)。任务队列的选择:有界队列,无界队列,sychronizedQueue,priorityBlockingQueue。饱和策略的选择:中止,抛弃,discardOldestPolicy,调用者运行。

 

推荐阅读