参考书籍:《Java并发编程的艺术》
Java中的线程池是运用场景非常多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池,线程池主要有三个好处:
**1、降低资源消耗。**通过重复利用一创建的线程会降低线程创建和销毁带来的消耗。
**2、提高响应速度。**当任务到达时,任务可以不需要等到线程创建就能立即执行。
**3、提高线程的可管理性。**线程是稀缺资源,需要线程池统一分配、调优和监控。
1 线程池的实现原理
当提交一个新任务到线程池中时,有下面的处理流程:
1、判断核心线程池里的线程是否都在执行任务,如果不是,则创建一个新的工作线程来执行任务;如果是,则进入下一个流程。
2、判断工作队列是否已满,如果未满则将新提交的任务存储在这个工作队列;如果已经满了,则进入下一个工作流程。
3、判断线程池里的线程是否已经满了,如果没有满,则创建一个新的工作线程来执行任务;如果满了,则交给饱和策略来处理这个任务。
ThreadPoolExecutor执行execute方法分下面四种情况:
1、如果当前运行的线程数少于corePoolSize,则创建新县城来执行任务(这一步需要获取全局锁)
2、如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue
3、如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务(这一步需要获取全局锁)
4、如果创建新线程使得当前运行线程超出maximumPoolSize,任务被拒绝,并调用RejectedExecutionHandler.rejectedExecution()方法
ThreadPoolExecutor采取上述步骤的总体设计思路是在执行execute方法是,尽可能避免获取全局锁。在ThreadPoolExecutor完成预热后(当前运行的线程数大于等于corePoolSize),几乎所有的execute方法调用都是执行步骤2,因为步骤2不需要获取全局锁。
ThreadPoolExecutor线程执行任务有两种情况:
1、在execute方法中创建一个线程,会让这个线程执行任务
2、在这个线程执行完下图中1的任务后,反复从BlockingQueue获取任务来执行
2 线程池的使用
2.1 线程池的创建
创建线程池需要输入下面几个参数:
- corePoolSize:线程池的基本大小。只要线程池中的线程少于这个参数,提交新任务时就会创建新线程来执行任务。
- maximumPoolSize:线程池最大数量。如果队列满了并且创建的线程数小于这个参数,会继续创建新的线程执行任务。
- keepAliveTime:线程活动保持时间。线程池的工作线程空闲后,保持存活的时间。
- unit:线程活动保持时间的单位。
- workQueue:任务队列。用来保存等待执行任务的阻塞队列。
- threadFactory:用于创建线程的工厂。
- handler:饱和策略。当线程池和队列都满了,即处于饱和,这时采取这个策略处理提交的新任务。默认是AbortPolicy,无法处理新任务时抛出异常。
2.2 向线程池提交任务
有两个方法可以向线程池提交任务,分别为execute和submit。execute方法用于提交不需要返回值的任务,所以无法判断是否被线程池执行。submit方法用于提交需要返回值的任务,线程池会返回Future类型的对象,通过这个对象可以判断任务是否执行成功,可以通过future的get方法获得返回值,并且会阻塞进程直到任务完成。
2.3 关闭线程池
调用shutdown或shutdownNow关闭线程池,原理是遍历线程池中工作的线程,诸葛调用interrupt方法中断线程,所以无法响应中断的方法无法终止。通常调用shutdown方法关闭线程池,线程池的状态设为SHUTDOWN,此时不能再往线程池添加任务,但是线程池不会立刻退出,而是等待线程池中的所有任务都执行完成。如果任务不一定要执行完,可以调用shutdownNow方法,线程池状态为STOP,停止所有线程,返回未执行的任务。
2.4 合理配置线程池
建议使用有界队列,有界队列能增加系统的稳定性和预警能力,大小可以设置大一些,例如几千。下面是不同类型的任务的合理配置方法。
- 任务的性质:CPU密集型、IO密集型和混合型
CPU密集型任务应该配置尽可能小的线程,如N+1,因为CPU密集型计算量很大,CPU一直在执行任务,切换线程开销大,尽量避免切换线程。IO密集型任务由于CPU不是一直在执行任务,可以配置尽可能多的线程,防止CPU资源浪费,如2N。混合型任务可以拆分成CPU密集型和IO密集型。 - 任务的优先级:高、中、低
可以用优先级队列PriorityBlockingQueue来处理,可以让优先级高的任务先执行,但可能造成饥饿。 - 任务的执行时间:长、中、短
可以交给不同规模的线程池,或者使用优先级队列,执行时间短的优先执行。 - 任务的依赖性:例如数据库连接
依赖数据库的任务,因为可能等待数据库返回结果的时间较长,因此线程数可以设置大一些,这样才能更好利用CPU。
2.5 线程池的监控
如果系统大量使用线程池,有必要对线程池进行监控,方便出现问题时快速定位。通过线程池提供的参数进行监控:
- taskCount:线程池需要执行的任务数量
- completedTaskCount:线程池已经完成的任务数量
- largestPoolSize:线程池曾经创建的最大线程数量
- getPoolSize:线程池的线程数量
- getActiveCount:获取活动的线程数
此外,也可以通过集成线程池来自定义线程池,在beforeExecution,afterExecution和Terminated等方法中加入想要的功能,例如监控任务平均执行时间等。