Java线程池详解

线程池主要解决以下两个问题:

1)提升性能:线程池能独立负责线程的创建、维护和分配。在执行大量异步任务时,可以不需要自己创建线程,而是将任务交给线程池去调度。线程池能尽可能使用空闲的线程去执行异步任务,最大限度地对已经创建地线程进行复用,使得性能提升明显。

2)线程管理:每个Java线程池会保持一些基本地线程统计信息,例如完成地任务数量、空闲时间等,以便对线程进行有效管理,使得能对所接收地异步任务进行高效调度。

1、Executors的4种快捷创建线程池的方法

Java通过Executors工厂类提供了4种快捷创建线程池的方法:

(1)newSingleThreadExecutor创建“单线程化线程池”

该方法用于创建一个“单线程化线程池”,也就是只有一个线程的线程池,所创建的线程池用唯一的工作线程来执行任务,使用此方法创建的线程池能保证所有任务按照指定顺序(如FIFO)执行。

该线程池有以下特点:

1)单线程化的线程池中的任务是按照提交的顺序执行的。

2)池中的唯一线程的存活时间是无限的。

3)当池中的唯一线程正繁忙时,新提交的任务实例会进入内部的阻塞队列中,并且其阻塞队列是无界的。

适用的场景是:任务按照提交次序,一个任务一个任务地逐个执行的场景。

(2)newFixedThreadPool创建“固定数量的线程池”

该方法用于创建一个“固定数量的线程池”,其唯一的参数用于设置池中线程的“固定数量”。

该线程池有以下特定:

1)如果线程池没有达到“固定数量”,每次提交一个任务线程池内就创建一个新线程,直到线程达到线程池固定的数量。

2)线程池的大小一旦达到“固定数量”就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。

3)在接收异步任务的执行目标实例时,如果池中的所有线程均在繁忙状态,新任务会进入阻塞队列中(无界的阻塞队列)。

适用的场景是:需要任务长时间的场景。

注:内部使用无界队列来存放排队任务,当大量任务超过线程池最大容量需要处理时,队列无限增大,使服务器资源迅速耗尽。

(3)newCachedThreadPool创建“可缓存线程池”

该方法用于创建一个“可缓存线程池”,如果线程池内的某些线程无事可干成为空闲线程,“可缓存线程池”可灵活回收这些空闲线程。

该线程池有以下特点:

1)在接收新的异步任务target执行目标实例时,如果池内所有线程繁忙,此线程池就会添加新线程来处理任务。

2)此线程池不会对线程池大小进行限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。

3)如果部分线程空闲,也就是存量线程的数量超过了处理任务数量,就会回收空闲(60秒不执行任务)线程。

适用的场景是:需要快速处理突发性强、耗时较短的任务场景。

注:线程池没有最大线程数量限制,如果大量的异步任务执行目标实例同时提交,可能会因创建线程过多而导致资源耗尽。

(4)newScheduledThreadPool创建“可调度线程池”

该方法用于创建一个“可调度线程池”,即一个提供“延时”和“周期性”任务调度功能的ScheduledExecutorService类型的线程池。

适用的场景是:周期性地执行任务地场景。

2、Executors快捷创建线程池的潜在问题

(1)SingleThreadPool和FixedThreadPool

这两个工厂方法所创建的线程池,任务队列的长度都为Integer.MAX_VALUE,可能会堆积大量的任务,从而导致OOM。

(2)CachedThreadPool和ScheduledThreadPool

这两个工厂方法所创建的线程池允许创建的线程数量为Integer.MAX_VALUE,可能会导致创建大量的线程,从而导致OOM。

3、线程池的标准创建方式

大部分企业的开发规范都会禁止使用快捷方法去创建线程池,而是要求通过标准构造器ThreadPoolExecutor去构造线程池。Executors工厂类中创建线程池的快捷方法实际上就是调用ThreadPoolExecutor(定时任务使用ScheduledThreadPoolExecutor)线程池的构造方法完成的。ThreadPoolExecutor构造方法有多个重载版本,其中一个比较重要的构造器如下:

public ThreadPoolExecutor(

    int corePoolSize,    // 核心线程数,即使线程空闲,也不会回收

    int maximumPoolSize,    // 最大线程数

    long keepAliveTime,    // 线程最大空闲时长

    TimeUnit unit,    // 空闲时长单位

    BlockingQueue workQueue,    // 任务阻塞队列

    ThreadFactory threadFactory,    // 新线程的产生方式

    RejectedExecutionHandler handler    // 拒绝策略

)

4、线程池的任务调度流程

线程池的任务调度流程大致如下:

1)如果线程池中总的工作线程数量小于核心线程数量(corePoolSize),接收到新任务,执行器会优先创建一个核心的工作线程,而不是从线程队列中获取一个空闲线程。

2)如果线程池中总的工作线程数量等于核心线程数量,接收到新的任务将被加入任务阻塞队列中,直到任务阻塞队列满为止。

3)当完成一个任务的执行时,执行器总是优先从任务阻塞队列中获取下一个任务,并开始执行,一直到任务阻塞队列为空。

4)在核心线程数量已经用完、任务阻塞队列已经满了的情况下,如果线程池接收到新的任务,将会为新任务创建一个非核心的工作线程,并且立即开始执行新任务。

5)在核心线程数量已经用完、任务阻塞队列已经满了的情况下,一直会创建非核心的工作线程去执行新任务,直到线程池内的工作线程总数超出最大线程数(maximumPoolSize),当新任务过来时,会为新任务执行拒绝策略。

6)当非核心的工作线程任务完成后空闲时间超过线程最大空闲时长(keepAliveTime),会被线程池回收。

总体的线程池的任务调度流程如下图所示:

5、任务阻塞队列

任务阻塞队列(BlockingQueue)与普通队列相比有一个重要的特点:在任务阻塞队列为空时会阻塞当前线程的元素获取操作。

其比较常用的实现类有:

1)ArrayBlockingQueue:是一个数组实现的有界阻塞队列,队列中的元素按FIFO排序。ArrayBlockingQueue在创建时必须设置大小,接收的任务超出corePoolSize数量时,任务被缓存到该任务阻塞队列中。

2)LinkedBlockingQueue:是一个基于链表实现的阻塞队列,按FIFO排序任务,可以设置容量(有界队列),不设置容量则默认使用Integer.Max_VALUE作为容量(无界队列)。该队列的吞吐量高于ArrayBlockingQueue。快捷工厂方法Executors.newSingleThreadExecutor和newSingleThreadExecutor所创建的线程池使用此队列,并且都没有设置容量。

3)PriorityBlockingQueue:是具有优先级的无界队列。

4)DelayQueue:是一个无界阻塞延迟队列,底层基于PriorityBlockingQueue实现,队列中每个元素都有过期时间,当从队列获取元素(元素出队)时,只有已经过期的元素才会出队,队列头部的元素是过期最快的元素。快捷工厂方法Executors.newScheduledThreadPool所创建的线程池使用此队列。

5)SynchronousQueue:(同步队列)是一个不存储元素的任务阻塞队列,每个插入操作必须等到另一个线程的调用移除操作,否则插入操作一直处于阻塞状态,其吞吐量通常高于LinkedBlockingQueue。快捷工厂方法Executors.newCachedThreadPool所创建的线程池使用此队列。

6、线程池的拒绝策略

在线程池的任务缓存队列为有界队列(有容量限制的队列)的时候,如果队列满了,提交任务到线程池的时候就会被拒绝。总体来说,任务被拒绝有两种情况:

1)线程池已经被关闭。

2)工作队列已满且maximumPoolSize已满。

无论以上哪种情况任务被拒绝,线程池都会调用RejectedExecutionHandler实例的rejectedExecution方法。RejectedExecutionHandler是拒绝策略的接口,JUC为该接口提供了以下几种实现:

1)AbortPolicy:拒绝策略。使用该策略时,如果线程池队列满了,新任务就会被拒绝,并且抛出RejectedExecutionException异常。该策略是线程池默认的拒绝策略。

2)DiscardPolicy:抛弃策略。使用该策略时,如果线程池队列满了,新任务就会直接被丢掉,并且不会有任何异常抛出。

3)DiscardOldestPolicy:抛弃最老任务策略。抛弃最老任务策略,也就是说如果队列满了,就会将最早进入队列的任务抛弃,从队列中腾出空间,再尝试加入队列。因为队列是队尾进队头出,队头元素是最老的,所以每次都是移除队头元素后再尝试入队。

4)CallerRunsPolicy:调用者执行策略。在新任务被添加到线程池时,如果添加失败,那么提交任务线程会自己去执行该任务,不会使用线程池中的线程去执行新任务。

5)自定义策略。如果以上拒绝策略都不符合需求,那么可自定义一个拒绝策略,实现RejectedExecutionHandler接口的rejectedExecution方法即可。

7、向线程池提交任务的两种方式

向线程池提交任务地两种方式为:调用execute()方法和调用submit()方法。

两种方法的区别:

1)execute()方法只能接收Runnable类型的参数,而submit()方法可以接收Callable、Runnable两种类型的参数。

2)submit()提交任务后会有返回值,而execute()没有。

3)submit()方便Exception处理。

8、线程池的生命周期

线程池的5种状态具体如下:

1)RUNNING(运行状态):线程池创建之后的初始状态,这种状态下可以执行任务。

2)SHUTDOWN(关闭状态):该状态下线程池不再接受新任务,但是会将任务队列中的任务执行完毕。

3)STOP(停止状态):该状态下线程池不再接受新任务,也不会处理任务队列中的剩余任务,并且将会中断所有工作线程。

4)TIDYING(整理状态):该状态下所有任务都已终止或者处理完成,将会执行terminated()钩子方法。

5)TERMINATED(结束状态):执行完terminated()钩子方法之后的状态。

线程池的状态转换规则为:

1)线程池创建之后状态为RUNNING。

2)执行线程池的shutdown()实例方法,会使线程池状态从RUNNING转变为SHUTDOWN。

3)执行线程池的shutdownNow()实例方法,会使线程池状态从RUNNING转变为STOP。

4)当线程池处于SHUTDOWN状态时,执行其shutdownNow()方法会将其状态转变为STOP。

5)等待线程池的所有工作线程停止,工作队列清空之后,线程池状态会从STOP转变为TIDYING。

6)执行完terminated()钩子方法之后,线程池状态从TIDYING转变为TERMINATED。

线程池的状态转换规则如下图所示:

你可能感兴趣的:(Java线程池详解)