Java线程池的使用方式,核心运行原理、以及注意事项
- 线程池的缘由
- 线程池的处理流程
- 线程池的使用(ThreadPoolExecutor)
- 线程池的注意事项
线程池的缘由
java中为了提高并发度,可以使用多线程共同执行,但是如果有大量线程短时间之内被创建和销毁,会占用大量的系统时间,影响系统效率。
为了解决上面的问题,java中引入了线程池,可以使创建好的线程在指定的时间内由系统统一管理,而不是在执行时创建,执行后就销毁,从而避免了频繁创建、销毁线程带来的系统开销。
线程池如何使用,以及实现原理,处理步骤,有什么使用注意事项等,今天主要从这几个方面详细介绍Java线程池。
线程池的处理流程
就以ThreadPoolExecutor为例,当我们把一个Runnable交给线程池去执行的时候,这个线程池处理的流程是这样的:
先判断线程池中的核心线程们是否空闲,如果空闲,就把这个新的任务指派给某一个空闲线程去执行。如果没有空闲,并且当前线程池中的核心线程数还小于 corePoolSize,那就再创建一个核心线程。
如果线程池的线程数已经达到核心线程数,并且这些线程都繁忙,就把这个新来的任务放到等待队列中去。如果等待队列又满了,那么查看一下当前线程数是否到达maximumPoolSize,如果还未到达,就继续创建线程。
如果已经到达了,就交给RejectedExecutionHandler(拒绝策略)来决定怎么处理这个任务。
线程池的使用(ThreadPoolExecutor)
在Java中,线程池的概念是Executor这个接口,具体实现为ThreadPoolExecutor类,是线程池中最核心的一个类,因此如果要透彻地了解Java中的线程池,必须先了解这个类。
ThreadPoolExecutor继承了AbstractExecutorService类,并提供了四个构造器:
public class ThreadPoolExecutor extends AbstractExecutorService {
…..
public ThreadPoolExecutor( int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue);
public ThreadPoolExecutor( int corePoolSize,
int maximumPoolSize,l
ong keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory);
public ThreadPoolExecutor( int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit nit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler);
public ThreadPoolExecutor( int corePoolSize,
int maximumPoolSize,l
ong keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler);
…
}
ThreadPoolExecutor继承了AbstractExecutorService类,并提供了四个构造器,事实上,通过观察每个构造器的源码具体实现,发现前面三个构造器都是调用的第四个构造器进行的初始化工作。下面解释下一下构造器中各个参数的含义:
- corePollSize:核心线程数。
在创建了线程池后,线程中没有任何线程,等到有任务到来时才创建线程去执行任务。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中。 - maximumPoolSize:最大线程数。
表明线程中最多能够创建的线程数量。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是如果使用了无界的任务队列这个参数就没什么效果。 - keepAliveTime:空闲的线程保留的时间。
所以如果任务很多,并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率。 - TimeUnit:空闲线程的保留时间单位。
• –TimeUnit.DAYS; //天
–TimeUnit.HOURS; //小时
–TimeUnit.MINUTES; //分钟
–TimeUnit.SECONDS; //秒
–TimeUnit.MILLISECONDS; //毫秒
–TimeUnit.MICROSECONDS; //微妙
–TimeUnit.NANOSECONDS; //纳秒
- BlockingQueue:阻塞队列,存储等待执行的任务。参数有
–ArrayBlockingQueue、
–LinkedBlockingQueue、
–SynchronousQueue可选。 - ThreadFactory:线程工厂,用来创建线程
可以通过线程工厂给每个创建出来的线程设置更有意义的名字,Debug和定位问题时非常又帮助。 - RejectedExecutionHandler:队列已满,而且任务量大于最大线程的异常处理策略。有以下取值
– ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
– ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
–ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行当前任务(重复此过程)
–ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任
线程池的注意事项
虽然线程池能大大提高服务器的并发性能,但使用它也会存在一定风险。与所有多线程应用程序一样,用线程池构建的应用程序容易产生各种并发问题,如对共享资源的竞争和死锁。此外,如果线程池本身的实现不健壮,或者没有合理地使用线程池,还容易导致与线程池有关的死锁、系统资源不足和线程泄漏等问题。
- 建议使用new ThreadPoolExecutor(…)的方式创建线程池
线程池的创建不应使用Executors 去创建,而应该通过 ThreadPoolExecutor创建,这样可以让读者更加明确地知道线程池的参数设置、运行规则,规避资源耗尽的风险,这一点在也阿里巴巴JAVA开发手册中也有明确要求。这一点不容小觑,曾有同学因为线程池使用不当导致生产的同一台机器上部署的多个应用都因无法创建线程池而出现故障。 - 合理设置线程数
线程池的工作线程数设置应根据实际情况配置,CPU密集型业务(搜索、排序等)CPU空闲时间较少,线程数不能设置太多。如果是CPU密集型任务,就需要尽量压榨CPU,参考值可以设为 NCPU+1;如果是IO密集型任务,参考值可以设置为2*NCPU - 设置能代表具体业务的线程名称
这样方便通过日志的线程名称识别所属业务。具体实现可以通过指定ThreadPoolExecutor的ThreadFactory参数。如使Spring提供的CustomizableThreadFactory。