3.2 线程池的工作原理
Java线程池主要用于管理线程组及其运行状态,以便Java虚拟机更好地利用CPU资源。Java线程池的工作原理为:JVM先根据用户的参数创建一定数量的可运行的线程任务,并将其放入队列中,在线程创建后启动这些任务,如果线程数量超过了最大线程数量(用户设置的线程池大小),则超出数量的线程排队等候,在有任务执行完毕后,线程池调度器会发现有可用的线程,进而再次从队列中取出任务并执行。
线程池的主要作用是线程复用、线程资源管理、控制操作系统的最大并发数,以保证系统高效(通过线程资源复用实现)且安全(通过控制最大线程并发数实现)地运行。
3.2.1 线程复用
在Java中,每个Thread类都有一个start方法。在程序调用start方法启动线程时,Java虚拟机会调用该类的run方法。前面说过,在Thread类的run方法中其实调用了Runnable对象的run方法,因此可以继承Thread类,在start方法中不断循环调用传递进来的Runnable对象,程序就会不断执行run方法中的代码。可以将在循环方法中不断获取的Runnable对象存放在Queue中,当前线程在获取下一个Runnable对象之前可以是阻塞的,这样既能有效控制正在执行的线程个数,也能保证系统中正在等待执行的其他线程有序执行。这样就简单实现了一个线程池,达到了线程复用的效果。
3.2.2 线程池的核心组件和核心类
Java线程池主要由以下4个核心组件组成。
◎ 线程池管理器:用于创建并管理线程池。
◎ 工作线程:线程池中执行具体任务的线程。
◎ 任务接口:用于定义工作线程的调度和执行策略,只有线程实现了该接口,线程中的任务才能够被线程池调度。
◎ 任务队列:存放待处理的任务,新的任务将会不断被加入队列中,执行完成的任务将被从队列中移除。
Java中的线程池是通过Executor框架实现的,在该框架中用到了Executor、Executors、ExecutorService、ThreadPoolExecutor、Callable、Future、FutureTask这几个核心类,具体的继承关系如图3-2所示。
图3-2
其中,ThreadPoolExecutor是构建线程的核心方法,该方法的定义如下:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), defaultHandler); }
ThreadPoolExecutor构造函数的具体参数如表3-1所示。
表3-1
3.2.3 Java线程池的工作流程
Java线程池的工作流程为:线程池刚被创建时,只是向系统申请一个用于执行线程队列和管理线程池的线程资源。在调用execute()添加一个任务时,线程池会按照以下流程执行任务。
◎ 如果正在运行的线程数量少于corePoolSize(用户定义的核心线程数),线程池就会立刻创建线程并执行该线程任务。
◎ 如果正在运行的线程数量大于等于corePoolSize,该任务就将被放入阻塞队列中。
◎ 在阻塞队列已满且正在运行的线程数量少于maximumPoolSize时,线程池会创建非核心线程立刻执行该线程任务。
◎ 在阻塞队列已满且正在运行的线程数量大于等于maximumPoolSize时,线程池将拒绝执行该线程任务并抛出RejectExecutionException异常。
◎ 在线程任务执行完毕后,该任务将被从线程池队列中移除,线程池将从队列中取下一个线程任务继续执行。
◎ 在线程处于空闲状态的时间超过keepAliveTime时间时,正在运行的线程数量超过corePoolSize,该线程将会被认定为空闲线程并停止。因此在线程池中所有线程任务都执行完毕后,线程池会收缩到corePoolSize大小。
具体的流程如图3-3所示。
图3-3
3.2.4 线程池的拒绝策略
若线程池中的核心线程数被用完且阻塞队列已排满,则此时线程池的线程资源已耗尽,线程池将没有足够的线程资源执行新的任务。为了保证操作系统的安全,线程池将通过拒绝策略处理新添加的线程任务。JDK内置的拒绝策略有AbortPolicy、CallerRunsPolicy、DiscardOldestPolicy、DiscardPolicy这4种,默认的拒绝策略在ThreadPoolExecutor中作为内部类提供。在默认的拒绝策略不能满足应用的需求时,可以自定义拒绝策略。
1.AbortPolicy
AbortPolicy直接抛出异常,阻止线程正常运行,具体的JDK源码如下:
public static class AbortPolicy implements RejectedExecutionHandler { public AbortPolicy() { } public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { //直接抛出异常信息,不做任何处理 throw new RejectedExecutionException("Task " + r.toString() + " rejected from " +e.toString()); } }
2.CallerRunsPolicy
CallerRunsPolicy的拒绝策略为:如果被丢弃的线程任务未关闭,则执行该线程任务。注意,CallerRunsPolicy拒绝策略不会真的丢弃任务。具体的JDK实现源码如下:
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { if (! e.isShutdown()) { r.run(); //执行被丢弃的任务r } }
3.DiscardOldestPolicy
DiscardOldestPolicy的拒绝策略为:移除线程队列中最早的一个线程任务,并尝试提交当前任务。具体的JDK实现源码如下:
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { if (! e.isShutdown()) { e.getQueue().poll(); //丢弃(移除)线程队列中最老(最后)的一个线程任务 e.execute(r); //尝试提交当前任务 } }
4.DiscardPolicy
DiscardPolicy的拒绝策略为:丢弃当前的线程任务而不做任何处理。如果系统允许在资源不足的情况下丢弃部分任务,则这将是保障系统安全、稳定的一种很好的方案。具体的JDK实现源码如下:
//直接丢弃线程,不做任何处理 public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { }
5.自定义拒绝策略
以上4种拒绝策略均实现了RejectedExecutionHandler接口,若无法满足实际需要,则用户可以自己扩展RejectedExecutionHandler接口来实现拒绝策略,并捕获异常来实现自定义拒绝策略。下面实现一个自定义拒绝策略DiscardOldestNPolicy,该策略根据传入的参数丢弃最老的N个线程,以便在出现异常时释放更多的资源,保障后续线程任务整体、稳定地运行。具体的JDK实现源码如下:
public class DiscardOldestNPolicy implements RejectedExecutionHandler { private int discardNumber = 5; private List<Runnable> discardList =new ArrayList<Runnable>(); public DiscardOldestNPolicy (int discardNumber) { this.discardNumber = discardNumber; } public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { if(e.getQueue().size() > discardNumber){ //step 1:批量移除线程队列中的discardNumber个线程任务 e.getQueue().drainTo(discardList, discardNumber); discardList.clear(); //step 2:清空discardList列表 if (! e.isShutdown()) { e.execute(r); //step 3:尝试提交当前任务 } } } }