为什么需要线程池
- 线程池的频繁创建和销毁和耗费系统资源,如果没有线程池,就要经常创建和销毁线程,这样就会降低程序的响应速度,相反则加快响应速度。
- 没有线程池,如果完成一个功能要反复创建和销毁100个线程,这100个线程使用次数就为1次,如果引入了线程池,可能就只需要创建20到30个线程,可以让先执行完任务的线程不必先销毁,让其再执行下一个任务,这样有效提高了每个线程的使用效率,也有效减少了线程的反复创建和销毁。
- 为了减少创建和销毁线程的次数,让每个线程可以多次使用,可根据系统情况调整执行的线程数量,防止消耗过多内存,所以我们可以使用线程池。
- 线程池可以统一管理线程(比如统一启动,停止),并且还方便收集数据。
线程池介绍
ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
- 线程池构造函数参数
参数名 | 类型 | 含义 |
---|---|---|
corePoolSize | int | 核心线程数,新创建的线程池是没有线程,当新创建的线程数量超过corePoolSize后,线程池在销毁超过keepAliveTime时间的线程时候,至少也会保留corePoolSize个线程存活。 |
maximumPoolSize | int | 最大线程数,线程池中最多能创建线程的数量,maximumPoolSize 不能小于corePoolSize。 |
keepAliveTime | long | 线程存活时间,多余corePoolSize的线程如果超过存活时间就会被回收。 |
unit | TimeUnit | 线程存活时间的时间单位。 |
workQueue | BlockingQueue<Runnable> | 任务存储队列,当任务进来时,线程数如果超过了corePoolSize,则将任务存储在workQueue队列。 |
threadFactory | ThreadFactory | 线程工厂类,当线程池需要新的线程时,会使用threadFactory来生成新的线程。 |
handler | RejectedExecutionHandler | 线程无法接受新的任务时的拒绝策略,当任务队列已满并且线程数达到maximumPoolSize不能创建新的线程时,此时不能接受任何任务的一种对任务拒绝的处理方式。 |
- 添加线程规则
- 如果线程数小于corePoolSize,即使其他工作线程处于空闲状态,也会创建一个新的线程来运行新的任务。
- 如果线程数大于等于corePoolSize但少于maximumPoolSize,则将任务放入队列。
- 如果队列已满,并且线程数小于maximumPoolSize,则创建一个新线程来运行任务。
- 如果队列已满,并且线程数大于等于maximumPoolSize,则拒绝该任务。
- 增减线程特点
- 设置corePoolSize和maximumPoolSize相等,可以固定大小的线程池,此时设置keepAliveTime是无效的,因为即使线程超时也不会回收核心线程。
- 线程池希望保持较少的线程数,只有在任务队列负载很大时才会创建新的线程。
- 只有队列满时才会创建大于corePoolSize的线程,如果使用的是无界队列(LinkedBlockingQueue),那么线程数就不会超过corePoolSize。
-
threadFactory(线程工厂)
默认的线程工厂是Executors.defaultThreadFactory(),该工厂创建出来的线程都在同一个线程组,拥有相同的优先级并且都不是守护线程。如果自己指定线程工厂,就可以自定义线程名,线程组和优先级等。 -
workQueue(工作队列)
- SynchronousQueue(交换队列):该队列不存储任何元素,本身没有容量,它的size()永远返回0。如果队列不为空,生产者线程put一个元素进队列时,必须等待消费者线程take前一个元素;相反,如果队列为空,消费者线程take一个元素出队时,必须等待生产者线程put一个元素。
- LinkedBlockingQueue(无界队列):该队列不会被塞满,可以装下任意多的任务。如果corePool(核心队列)都在工作,无论设置多大的maximumPoolSize(最大线程数)都是无效的。如果核心线程处理任务的速度跟不上任务提交的速度,就会导致LinkedBlockingQueue任务越来越多,可能导致内存浪费或者内存溢出(OOM)的情况。
- ArrayBlockingQueue(有界队列):可以设置队列大小,当队列任务满的时候并且线程数还没有达到最大值,才会创建新的队列,此时设置的maximumPoolSize的值才会生效。
- JDK提供自动创建队列的方式和特点
- Executors.newFixedThreadPool(nThreads):创建固定数量线程池;形参nThreads指定创建多少个线程,底层其实也是用了
new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
此时的corePoolSize和maximumPoolSize值都是相同的,由上可知,keepAliveTime此时是无效的,所以值为0。队列使用了LinkedBlockingQueue无界队列,即使maximumPoolSize大于corePoolSize,最大的线程数也不会超过corePoolSize。所以以上种种参数就能实现创建固定数量的线程。 - Executors.newSingleThreadExecutor():创建1个线程的线程池;这个方法没有参数,创建多少个线程源码中已经写死了。
new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
,本质就是Executors.newFixedThreadPool(1)的变形。 - Executors.newCachedThreadPool():可缓存的线程池;底层源码
new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
,corePoolSize为0,maximumPoolSize为Integer.MAX_VALUE,可以认为最大线程数无上限,默认超时时间为60秒,队列为没有容量的SynchronousQueue的交换队列。该线程池的特点为每提交一个任务,都会创建一个新的线程来执行任务,执行完任务或者超时的线程将会被回收,新的任务将会创建新的线程。如果任务非常多,创建的线程也会非常多,也有可能导致OOM。 - Executors.newScheduledThreadPool(corePoolSize):支持定时以及周期性任务执行的线程池;参数corePoolSize为指定创建多少的核心线程数。底层源码本质为:
new ThreadPoolExecutor(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue());
DelayedWorkQueue为具有延迟功能的队列。该线程池执行的方法为:schedule(Runnable command, long delay, TimeUnit unit)和scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit),参数delay指定线程延迟多长时间后才开始执行,period为间隔多久执行一次任务。
注:一般而言,手动创建线程池会比使用JDK自动线程池方式会更好,因为这样可以让我们更加明确线程池的运行规则,避免资源浪费。
- 线程池的线程数设定多少才合适?
- CPU密集型(加密、计算hash等):最佳线程数为CPU核心树的1-2倍左右。
- 耗时IO型(读写数据库、文件读写,网络读写):由于IO操作大多数情况都会存在阻塞的情况,所以最佳线程数一般大于CPU核心数的很多倍。
- 通用型:线程数=CPU核心数 * (1 + 平均等待时间 / 平均工作时间)
- 停止线程池的方法
- shutdown():执行shtudown()方法只是不会接收新的任务,但是不会立马停止线程,而是等待线程池中正在执行的任务以及队列中待执行的任务执行完毕后再关闭线程池。执行了该方法,如果继续提交任务,则会使用相应线程池默认的拒绝策略拒绝新的任务(拒绝策略下面讲解)。
- shutdownNow():该方法不会停止正在执行的任务,只会停止待执行的任务,但也不接收新的任务。相对于shutdown()方法控制的粒度更细。该方法停止的待执行任务通过返回值(List类型)返回,我们可以拿到这些待执行的任务根据业务需求做相应的处理,比如开启新的线程执行,或者记录日志等等。
- 判断线程池停止的方法
- isShutdown():该方法可以判断是否已经下达了shutdown命令,但不可以判断此时线程池是否仍然在工作。
- isTerminated():该方法可以判断线程池是否已关闭,即当正在工作的线程执行完毕,并且任务队列的任务执行完毕,则返回true,否则返回false。
- awaitTermination(long timeout, TimeUnit unit):该方法用来判断在指定的时间内线程池内正在执行的任务或者待执行的任务是否执行完毕。相对于isTerminated()方法,该方法具有线程池指定时间内所有任务是否已经执行完毕了。
- 线程池的拒绝策略
线程池拒绝新任务的时机为:当线程池的任务队列满的时候;当执行了shutdown()方法之后;
- AbortPolicy:默认的拒绝策略;该拒绝策略会直接抛出RejectedExecutionException异常,表示不能接收新的任务了。
- DiscardPolicy:该拒绝策略会默默的丢弃不能接收的任务,调用者得不到被丢弃的信息。
- DiscardOldestPolicy:相对于DiscardPolicy,该策略将会丢弃最先的不能接收的任务。
- CallerRunsPolicy:该拒绝策略将不能接收的任务给提交任务上级线程来处理。比如子线程将不能接收的任务抛给提交任务的主线程来处理。
- 线程池状态:
- RUNNING:接收新任务,并且处理排队任务。
- SHUTDOWN:不接受新任务,但处理排队任务,即shutdown()执行完成。
- STOP:不接受新任务,也不处理排队任务,并中端正在执行的任务。
- TIDYING:所有任务都已经终止,线程会转到该状态,并将运行terminate()钩子方法。
- TERMINATED:所有任务都已经执行完成后的状态,即terminate()方法执行完成。