Java中的线程池是运营场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池,在开发过程中,合理地使用线程池能够带来3个好处:
- 降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度:当任务到达时,任务可以不需要等待线程创建就可以立即执行。
- 提高线程的可管理性:线程时稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。
线程池的实现原理
当提交一个新任务到线程池时,线程池的处理流程如下:
- 线程池判断核心线程池里的线程是否都在执行任务,如果不是,则创建一个新的工作线程来执行任务。如果核心线程池里的线程都在执行任务,则进入下个流程。
- 线程池判断工作队列是否已经满。如果没满,则将任务放入工作队列,等待核心线程池有空闲线程时,再取出来执行。如果满了,则进入下个流程。
- 线程池判断线程池的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果满了,则交给饱和策略取处理这个任务。
流程图如下:
线程池的创建
我们可以通过ThreadPoolExecutor来创建一个线程池:
创建一个线程池时需要输入的参数:
- corePoolSize(线程池的基本大小):当提交一个任务到线程池时,即使还有其他空闲的核心线程,线程池会创建一个线程来执行任务,直到核心线程等于corePoolSize时,就会将新提交的任务放入队列或创建新的工作线程。我们可以通过调用线程池的prestartAllCoreThreads()方法来提前创建并启动所有的核心线程。
- runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列。可以选择ArrayBolckingQueue、LinkedBlockingQueue、SynchronousQueue、PriorityBlockingQueue等阻塞队列。
- maximumPoolSize(线程池最大数量):如果队列已满,并且已创建的线程数小于maximumPoolSize,则线程池会创建新的线程执行任务。如果使用了无界队列,则此参数无效果。
- ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。
- RejectedExecutionHandler(饱和策略):如果队列已满,且已创建的线程数等于maximumPoolSize,则采取饱和策略处理新提交的任务。有以下4种策略:
a) AbortPolicy:直接抛出异常。
b) CallerRunsPolicy:使用调用者线程来执行任务。
c) DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
d) DiscardPolicy:不处理,直接丢弃任务。
我们也可以实现RejectedExecutionHandler接口来自定义饱和策略,如记录日志或持久化存储等。
- keepAliveTime(线程保持活动时间):线程池中空闲的线程存活时间。
- TimeUnit(线程保活时间的单位):可选的单位有天、小时、分钟、毫秒、微妙和纳秒。
向线程池提交任务
可以使用execute()或submit()方法向线程池提交任务。
execute()方法用于提交不需要返回值的任务,所以无法判断任务的执行状态。
submit()方法用于提交需要返回值的任务。线程池会返回一个Future类型的对象,通过此对象,我们可以判断任务是否执行成功,并通过get()方法阻塞当前线程直到任务完成并返回返回值。
ThreadPoolExecutore执行execute()方法的流程图
1.如果当前运行的线程少于corePoolSize,则创建新的线程执行任务(执行这一步骤需要获取全局锁。)
2.如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue。
3.如果BlockingQueue已满,其创建的线程小于maximumPoolSize,则创建新的线程执行任务(执行这一步骤需要获取全局锁。)
4.如果当前线程数量大于maximumPoolSize,则调用RejectedExecutionHandler.rejectedExecution()方法处理。
ThreadPoolExecutor采取上述步骤是为了在执行execute()方法是,尽可能的避免获取全局锁,因为获取全局锁是一个严重的可伸缩瓶颈。在线程数量达到corePoolSize后,几乎所有的execute()方法都处于上述的第2个步骤,而不需要获取全局锁。
ThreadPoolExecutor中线程执行任务示意图
1.在execute()方法中创建一个线程并执行当前任务。
2.该线程执行完当前任务后,不断从BlockingQueue获取任务来执行。
关闭线程池
通过调用线程池的shutdown或shutdownNow方法来关闭线程池。其原理是遍历线程池中的工作线程,然后依次调用线程的interrupt方法中断线程。两者的区别是shutdownNow方法会立即中断所有的工作线程,而shutdown方法只会中断未开始的任务线程并等待正在执行任务的线程执行完毕。可以调用isShutDown方法和isTerminaed方法来检查线程池及线程池中的工作线程关闭状态。
合理的配置线程池
要想合理的配置线程池,就必须首先分析任务特性,可以从以下几个角度分析。
- 任务的性质:CPU密集型任务、IO密集型任务和混合型任务。
- 任务的优先级:高、中和低。
- 任务的执行时间:长、中和短。
- 任务的依赖性:是否依赖其他系统资源,如数据库连接。
性质不同的任务可以用不同规模的线程池分开处理。CPU密集型任务应配置尽可能小的线程,如cpu个数+1个线程。而IO密集型任务应配置尽可能多的线程,如cpu数量*2个线程。因为IO密集型任务线程并不是一直在执行任务,可能被IO阻塞等,如果线程太少,则会导致无法及时处理新提交的任务。
线程池的监控
如果在系统中大量使用线程池,则有必要对线程池进行监控,方便在出现问题时,快速定位问题。可以通过线程池提供的以下参数进行监控:
- taskCount:线程池需要执行的任务数量。
- completedTaskCount:线程池在运行过程中已完成的任务数量,小于或等于taskCount。
- largestPoolSize:线程池里增加创建过的最大线程数量。通过这个数据可以知道线程池是否曾经满过。
- getPoolSize:线程池的线程数量。
- getActiveCount:获取活动的线程数。
也可以通过继承线程池来自定义线程池,重写线程池的beforeExecute、afterExecute和terminate方法,在任务执行前、执行后和线程池关闭前执行一些代码进行监控。
【备注】:本文图片均摘自《Java并发编程的艺术》·方腾飞,若本文有错或不恰当的描述,请各位不吝斧正。谢谢!