Offer来了:Java面试核心知识点精讲(原理篇)
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

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:尝试提交当前任务
          }
      }
    }
}