前言平时接触多线程开发的小朋友应该或多或少对线程池有一定的了解。之前在《阿里巴巴 Java 手册》也有一篇文章发表:线程池的重要性可见一斑。简单的说,使用线程池有以下几个目的:线程是稀缺资源,不能频繁创建。解耦功能;线程创建和执行完全分离,方便维护。它应该被放入一个池中,并且可以重复用于其他任务。线程池的原理说到线程池,大家就会想到池化技术。核心思想是把宝贵的资源放在一个池子里;每次用完就从中取出,用完后放回池子里供其他人使用。这有点像吃一大锅米饭。它是如何在Java中实现的?JDK1.5之后,引入了相关的API。常用的线程池创建方式如下:Executors.newCachedThreadPool():无限线程池。Executors.newFixedThreadPool(nThreads):创建一个固定大小的线程池。Executors.newSingleThreadExecutor():为单线程创建线程池。其实看这三种方式创建的源码就会发现:publicstaticExecutorServicenewCachedThreadPool(){returnnewThreadPoolExecutor(0,Integer.MAX_VALUE,60L,TimeUnit.SECONDS,newSynchronousQueue());}其实是使用ThreadPoolExecutor实现的班级。那么让我们关注ThreadPoolExecutor是如何工作的。首先是创建线程的API:ThreadPoolExecutor(intcorePoolSize,intmaximumPoolSize,longkeepAliveTime,TimeUnitunit,BlockingQueueworkQueue,RejectedExecutionHandlerhandler)这些核心参数的作用:corePoolSize是线程池的基本大小。maximumPoolSize是线程池的最大线程大小。keepAliveTime和unit是线程空闲后的存活时间。workQueue是一个用于存储任务的阻塞队列。handler队列和最大线程池都满时的饱和策略。了解了这些参数后,我们再来看实际应用。通常我们使用:threadPool.execute(newJob());这种方式提交任务到线程池,所以核心逻辑就是execute()函数。在具体分析之前,先了解一下线程池中定义的状态。这些状态与线程的执行密切相关:RUNNING自然是运行状态,表示可以接受任务执行队列中的任务。SHUTDOWN表示调用了shutdown()方法,不是再次Accept新任务,而是必须执行队列中的任务。STOP表示调用shutdownNow()方法,不再接受新的任务,阻塞队列中的所有任务被丢弃,所有正在执行的任务被中断。TIDYING所有任务都已执行,并会在调用shutdown()/shutdownNow()时尝试更新到此状态。TERMINATED是终止状态,执行terminated()时会更新到这个状态。用图表示:然后看execute()方法是如何处理的:获取当前线程池的状态。当当前线程数小于coreSize时,创建一个新线程运行。如果当前线程正在运行并且写入阻塞队列成功。仔细检查,再次获取线程状态;如果线程状态发生变化(非运行状态),需要将任务从阻塞队列中移除,并尝试判断线程是否全部执行完毕。同时执行拒绝政策。如果当前线程池为空,则新建一个线程并执行。如果第三步判断为非运行,则尝试创建新的线程,如果失败则执行拒绝策略。这里我们用一张《聊聊并发》的图来描述这个过程:如何配置线程?说完流程,我们来看看上面提到的核心参数怎么配置?有一点是肯定的,线程池是不是越大越好?好的。通常我们需要根据执行这些任务的性质来确定。IO密集型任务:由于线程并不是一直在运行,所以可以配置尽可能多的线程,比如CPU数*2CPU密集型任务(大量复杂操作)就应该分配较少的线程,比如由于CPU数量的大小是相等的。当然,这些都是经验值,最好的办法还是根据实际情况测试最合适的配置。优雅关闭线程池,有运行任务和自然关闭任务。从上面提到的五个状态,我们可以看出线程池是如何关闭的。其实无非就是两个方法:shutdown()/shutdownNow()。但它们有一个重要区别:shutdown()执行后停止接受新任务,并完成队列中的任务。shutdownNow()也停止接受新的任务,但是它会中断所有的任务并改变线程池的状态为停止。这两种方式都会中断线程,用户可以判断是否需要响应中断。shutdownNow()比较简单粗暴,可以根据实际场景选择不同的方法。我通常通过以下方式关闭线程池:longstart=System.currentTimeMillis();for(inti=0;i<=5;i++){pool.execute(newJob());}pool.shutdown();while(!pool.awaitTermination(1,TimeUnit.SECONDS)){LOGGER.info("线程还在执行...");}longend=System.currentTimeMillis();LOGGER.info("一共【{}】",(end-start));pool.awaitTermination(1,TimeUnit.SECONDS)会每秒检查是否执行完成(状态为TERMINATED),退出while循环时,表示线程池已经完全终止。2018年SpringBoot使用线程池,SpringBoot大行其道;让我们看看如何在SpringBoot中配置和使用线程池。既然使用了SpringBoot,自然要利用Spring的特性,所以需要Spring帮我们管理线程池:@ConfigurationpublicclassTreadPoolConfig{/***consumerqueuethread*@return*/@Bean(value="consumerQueueThreadPool")publicExecutorServicebuildConsumerQueueThreadPool()());returnpool;}}使用时:@Resource(name="consumerQueueThreadPool")privateExecutorServiceconsumerQueueThreadPool;@Overridepublicvoidexecute(){//consumerqueuefor(inti=0;i<5;i++){consumerQueueThreadPool.execute(newConsumerQueueThread());}}其实很简单,就是创建一个线程池bean,使用的时候直接从Spring拿来就可以了。监控线程池讲到了SpringBoot,它的执行器组件也可以用来监控线程池。线程是稀缺资源,对线程池的监控可以知道任务执行的状态和效率。我不会详细介绍执行器。如果您有兴趣,可以阅读这篇文章。已经详细梳理了如何暴露监控端点。其实ThreadPool本身已经提供了很多获取线程状态的API:很多方法看名字就知道意思。我们只需要将这些信息暴露给SpringBoot的监控端点,就可以在可视化页面上查看当前线程池的状态。我们甚至可以从线程池中继承几个函数来自定义监控逻辑:你可以看到这些名称和定义,它们是由子类实现的。自定义逻辑可以在线程的终止状态之前、之后和终止状态中执行。线程池隔离线程池看似很美好,但是也带来了一些问题。如果我们的很多业务都依赖同一个线程池,当其中一个业务因为各种不可控的原因消耗掉所有的线程时,线程池就会被占满。这样一来,其他业务将无法正常运营,对系统的打击是巨大的。比如我们的Tomcat接受请求线程池,假设有的响应很慢,线程资源无法回收释放;线程池逐渐变满,最坏的情况是整个应用无法提供服务。所以我们需要隔离线程池。通常的做法是按照业务来划分:比如下单任务使用一个线程池,获取数据任务使用另一个线程池。这样,即使其中一个任务出现问题,线程池耗尽,也不会影响其他任务的运行。Hystrix隔离了这样的需求Hystrix已经帮我们实现了。Hystrix是一个开源的容错插件,具有依赖隔离、系统容错降级等功能。我们来看一下Hystrix的简单应用:首先需要定义两个线程池,分别用于执行订单和处理用户。/***函数:订单服务**@authorcrossoverJie*日期:2018/7/2816:43*@sinceJDK1.8*/publicclassCommandOrderextendsHystrixCommand
