并发编程精华问答(八)| Java线程池使用时注意事项
虽然线程池能大大提高服务器的并发性能,但使用它也会存在一定风险。与所有多线程应用程序一样,用线程池构建的应用程序容易产生各种并发问题,如对共享资源的竞争和死锁。此外,如果线程池本身的实现不健壮,或者没有合理地使用线程池,还容易导致与线程池有关的死锁、系统资源不足和线程泄漏等问题。
01
Java线程池七个参数介绍
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
1、corePoolSize 线程池核心线程大小:
线程池中会维护一个最小的线程数量,即使这些线程处理空闲状态,他们也不会被销毁,除非设置了allowCoreThreadTimeOut。这里的最小线程数量即是corePoolSize。
2、maximumPoolSize 线程池最大线程数量:
一个任务被提交到线程池以后,首先会找有没有空闲存活线程,如果有则直接将任务交给这个空闲线程来执行,如果没有则会缓存到工作队列(后面会介绍)中,如果工作队列满了,才会创建一个新线程,然后从工作队列的头部取出一个任务交由新线程来处理,而将刚提交的任务放入工作队列尾部。线程池不会无限制的去创建新线程,它会有一个最大线程数量的限制,这个数量即由maximunPoolSize指定。
3、keepAliveTime 空闲线程存活时间:
一个线程如果处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁,这里的指定时间由keepAliveTime来设定。
4、unit 空闲线程存活时间单位:
keepAliveTime的计量单位。
5、workQueue 工作队列:
新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务。jdk中提供了四种工作队列:
- ArrayBlockingQueue:基于数组的有界阻塞队列,按FIFO排序。新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。当线程池中线程数量达到corePoolSize后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略。
- LinkedBlockingQuene:基于链表的无界阻塞队列(其实最大容量为Interger.MAX),按照FIFO排序。由于该队列的近似无界性,当线程池中线程数量达到corePoolSize后,再有新任务进来,会一直存入该队列,而不会去创建新线程直到maxPoolSize,因此使用该工作队列时,参数maxPoolSize其实是不起作用的。
- SynchronousQuene:一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。
- PriorityBlockingQueue:具有优先级的无界阻塞队列,优先级通过参数Comparator实现。
6、threadFactory 线程工厂:
创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程等等。
7、handler 拒绝策略:
当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,该如何处理呢。这里的拒绝策略,就是解决这个问题的,jdk中提供了4种拒绝策略:
- CallerRunsPolicy(用调用者所在线程来执行任务):该策略下,在调用者线程中直接执行被拒绝任务的run方法,除非线程池已经shutdown,则直接抛弃任务。
- AbortPolicy(直接抛出异常,这是默认策略):该策略下,直接丢弃任务,并抛出RejectedExecutionException异常。
- DiscardPolicy(直接丢弃任务):该策略下,直接丢弃任务,什么都不做。
- DiscardOldestPolicy(丢弃阻塞队列中最靠前的任务,并执行当前任务):该策略下,抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列。
02
线程池中重要参数的配置
corePoolSize : 核心线程数量;
workQueue : 等待队列;
maximumPoolSize : 最大线程数量。
提交任务时,判断的顺序为 corePoolSize -> workQueue ->maximumPoolSize。
- 当线程数小于核心线程数时,创建核心线程。
- 当线程大于等于核心线程数,且任务队列未满时,将任务放入队列。
- 当线程数大于核心线程数,且任务队列已满时,检查最大线程数是否已满,若未满,创建非核心线程,若满,根据拒绝策略抛出异常拒绝任务。
03
使用线程池遇到的问题(坑)
1、任务提交后长时间没有执行:
任务进入了队列,线程还在执行之前的任务。提交的任务还在排队等待执行中。
本质原因是对线程和队列的优先级认识不深刻,有一种错觉以为是所有线程都忙的时候才进入任务队列。实际上相反,是队列满的时候才会新建线程(线程数大于core size时)。
2、线程执行任务中无故消失:
- 线程拒绝策略配置为CallerRunsPolicy、DiscardOldestPolicy、DiscardPolicy时 ,线程池满了不会抛出异常。建议将拒绝策略配置为AbortPolicy。
- 一般情况下,代码中只会去捕捉RuntimeException,如果抛出Error(比如内存溢出)则会导致线程退出,而异常信息又没有拿到。最佳的解决办法是给线程池设置UncaughtExceptionHandler。
04
Java线程池使用时的注意事项
1、建议使用new ThreadPoolExecutor(...)的方式创建线程池:
线程池的创建不应使用 Executors 去创建,而应该通过 ThreadPoolExecutor 创建,这样可以让读者更加明确地知道线程池的参数设置、运行规则,规避资源耗尽的风险,这一点在也阿里巴巴JAVA开发手册中也有明确要求。这一点不容小觑,曾有同学因为线程池使用不当导致生产的同一台机器上部署的多个应用都因无法创建线程池而出现故障。
2、合理设置线程数:
- 线程池的工作线程数设置应根据实际情况配置,CPU密集型业务(搜索、排序等)CPU空闲时间较少,线程数不能设置太多。
- 如果是CPU密集型任务,就需要尽量压榨CPU,参考值可以设为 NCPU+1。
- 如果是IO密集型任务,参考值可以设置为2*NCPU。
3、设置能代表具体业务的线程名称:
这样方便通过日志的线程名称识别所属业务。具体实现可以通过指定ThreadPoolExecutor的ThreadFactory参数。如使Spring提供的CustomizableThreadFactory
END
来源 | 海兴
扫描二维码
获取更多精彩
海兴破颈记