Java线程池的运行原理以及使用详解

一、 为什么要使用线程池

在一些需要使用线程去处理任务的业务场景中,如果每一个任务都创建一个线程去处理,任务处理完过后,把这个线程销毁,这样会产生大量的线程创建,销毁的资源开销。使用线程池能有效的控制这种线程的创建和销毁,而且能够对创建的线程进行有效的管理。

二、Java线程池相关的API介绍

1. Executor接口

主要是用来执行提交的任务。下面是接口定义:

public interface Executor {

    void execute(Runnable command);

}

后面说的线程池会实现这个接口,并且会使用这个方法来提交一个任务。

2. ExecutorService接口

ExecutorService接口是Executor接口的一个子接口,它在Executor接口的基础上增加了一些方法,用来支持对任务的终止管理以及对异步任务的支持。

public interface ExecutorService extends Executor {

    void shutdown();

    boolean isShutdown();

    boolean isTerminated();

    boolean awaitTermination(long timeout, TimeUnit unit)
        throws InterruptedException;

     Future submit(Callable task);

     Future submit(Runnable task, T result);

    Future submit(Runnable task);

     List> invokeAll(Collection> tasks)
        throws InterruptedException;

     List> invokeAll(Collection> tasks,
                                  long timeout, TimeUnit unit)
        throws InterruptedException;

     T invokeAny(Collection> tasks)
        throws InterruptedException, ExecutionException;
     T invokeAny(Collection> tasks,
                    long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

3. AbstractExecutorService 抽象类

AbstractExecutorService实现了ExecutorService,并基于模板方法模式对一些方法给出了实现。是我们接下来要提到的线程池类ThreadPoolExecutor的直接父类。代码贴出来有点多,这里就不贴了。

4. ThreadPoolExecutor类

ThreadPoolExecutor通常就是我们所说的线程池类,Java的线程池就是用过这个类进行创建的。下面分析的线程池的运行原理,也是基于这个类来进行分析的。

5. ScheduledExecutorService接口

ScheduledExecutorService接口是ExecutorService子接口,定义了线程池基于任务调度的一些方法。

public interface ScheduledExecutorService extends ExecutorService {

    public ScheduledFuture schedule(Runnable command,
                                       long delay, TimeUnit unit);

    public  ScheduledFuture schedule(Callable callable,
                                           long delay, TimeUnit unit);

    public ScheduledFuture scheduleAtFixedRate(Runnable command,
                                                  long initialDelay,
                                                  long period,
                                                  TimeUnit unit);

    public ScheduledFuture scheduleWithFixedDelay(Runnable command,
                                                     long initialDelay,
                                                     long delay,
                                                     TimeUnit unit);

}

可以看到,上面定义了延时周期调度,固定频率周期调度,返回任务结果的任务调度等方法。

6. ScheduledThreadPoolExecutor类

ScheduledThreadPoolExecutor继承了ThreadPoolExecutor类,并且实现了ScheduledExecutorService接口,对任务调度的功能进行了实现。

7. Executors类

Executors可以认为是线程池的工厂类,里面提供了静态方法对线程池进行创建。
下面列出常用的几种线程池创建方法:

//固定线程大小的线程池
public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue());
    }

//单个线程线程池
public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue()));
    }

//无上限线程线程池
public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue());
    }

//基于任务调度的线程池(还有其他类型的任务调度线程池,这里不列举了)
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }

以上就是Java线程池相关的API类。

三、Java线程池的运行原理

这里主要介绍Java线程池各种情况下,一个任务提交给线程池,它在线程池内部是怎么运行的。在介绍内部运行机制之前,有必要先对线程池的一些参数属性进行介绍。

1. 参数属性介绍

  • 核心线程数corePoolSize:核心线程池数量。提交一个任务的时候,会对线程池里面的当前存活线程数和这个corePoolSize进行比较,不同的情况会有不同的操作。
  • 最大线程数maximumPoolSize:线程池所能创建的线程的最大的数量。
  • 空闲线程的超时时间keepAliveTime:如果线程池当前线程数是大于corePoolSize,并且这些线程中是有空闲线程的,也就是说这些线程没有在执行任务,那么空闲时间超过keepAliveTime时间,这些线程会被销毁,直到当前线程数等于corePoolSize,这时即使有空闲线程并且超时了也不会进行线程销毁。
  • 任务队列workQueue:这是一个阻塞队列,用于存储提交的任务。
  • 线程工厂threadFactory:线程池会使用这个工厂类来创建线程,用户可以自己实现。
  • 任务的拒绝处理handler(RejectedExecutionHandler):在线程数已经达到了最大线程数,而且任务队列也满了过后,提交的任务会使用这个handler来进行处理,用户也可以自己实现。默认是抛出一个异常RejectedExecutionException。

2. 线程池运行原理分析

上面介绍了线程池内部的一些核心属性,下面会基于这些属性来介绍,当用户提交一个任务时,线程池内部是如何运行的。

  1. 创建一个线程池,在还没有任务提交的时候,默认线程池里面是没有线程的。当然,你也可以调用prestartCoreThread方法,来预先创建一个核心线程。
  2. 线程池里还没有线程或者线程池里存活的线程数小于核心线程数corePoolSize时,这时对于一个新提交的任务,线程池会创建一个线程去处理提交的任务。当线程池里面存活的线程数小于等于核心线程数corePoolSize时,线程池里面的线程会一直存活着,就算空闲时间超过了keepAliveTime,线程也不会被销毁,而是一直阻塞在那里一直等待任务队列的任务来执行。
  3. 当线程池里面存活的线程数已经等于corePoolSize了,这是对于一个新提交的任务,会被放进任务队列workQueue排队等待执行。而之前创建的线程并不会被销毁,而是不断的去拿阻塞队列里面的任务,当任务队列为空时,线程会阻塞,直到有任务被放进任务队列,线程拿到任务后继续执行,执行完了过后会继续去拿任务。这也是为什么线程池队列要是用阻塞队列。
  4. 当线程池里面存活的线程数已经等于corePoolSize了,并且任务队列也满了,这里假设maximumPoolSize>corePoolSize(如果等于的话,就直接拒绝了),这时如果再来新的任务,线程池就会继续创建新的线程来处理新的任务,知道线程数达到maximumPoolSize,就不会再创建了。这些新创建的线程执行完了当前任务过后,在任务队列里面还有任务的时候也不会销毁,而是去任务队列拿任务出来执行。在当前线程数大于corePoolSize过后,线程执行完当前任务,会有一个判断当前线程是否需要销毁的逻辑:如果能从任务队列中拿到任务,那么继续执行,如果拿任务时阻塞(说明队列中没有任务),那超过keepAliveTime时间就直接返回null并且销毁当前线程,直到线程池里面的线程数等于corePoolSize之后才不会进行线程销毁。
  5. 如果当前的线程数达到了maximumPoolSize,并且任务队列也满了,这种情况下还有新的任务过来,那就直接采用拒绝的处理器进行处理。默认的处理器逻辑是抛出一个RejectedExecutionException异常。你也就可以指定其他的处理器,或者自定义一个拒绝处理器来实现拒绝逻辑的处理(比如讲这些任务存储起来)。JDK提供了四种拒绝策略处理类:AbortPolicy(抛出一个异常,默认的),DiscardPolicy(直接丢弃任务),DiscardOldestPolicy(丢弃队列里最老的任务,将当前这个任务继续提交给线程池),CallerRunsPolicy(交给线程池调用所在的线程进行处理)。

3. 常用的几种线程池以及使用场景

这里主要介绍使用Executors类来创建的几种线程池,及其比较适合的使用场景。

  • SingleThreadExecutor:单个线程的线程池。
public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue()));
    }

可以看到,线程池里最多一直有个线程用来处理任务,并且队列使用的是无界队列LinkedBlockingQueue。这种线程池主要适用于请求量非常小的场景,或者离线的数据处理等,只需要一个线程就够了。在持续的请求量比较大的情况下,不要使用这种线程池,单线程处理会使队列不断变大,最终可能导致oom(内存溢出)。

  • FixedThreadPool:固定线程大小线程池
public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue());
    }

可以看到corePoolSize和maximumPoolSize是相等的,keepAliveTime设置为0,队列用的是LinkedBlockingQueue无界队列。这种线程池适用于流量比较稳定的情况,不会说一段时间突然有大量的流量涌入,导致LinkedBlockingQueue越来越大最后导致内存溢出。

  • CachedThreadPool:按需求创建线程数量线程池
public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue());
    }

可以看到corePoolSize=0,maximumPoolSize是Integer.MAX_VALUE, keepAliveTime为60秒,队列使用的是SynchronousQueue同步队列,这个队列可以理解为没有容量的阻塞队列,只有有别的线程来拿任务时,当前线程任务才能插入成功,反过来也是一样。所以这种线程池任务队列是不存任务的,任务全靠创建新的线程来处理,处理完了过后线程空闲超过60秒就会被销毁。所以这种线程池适合有一定高峰流量的场景。但是还是要慎用,如果流量过高,会导致创建的线程过多,直接导致服务所在机器的CPU负载过高,然后机器卡死。所以如果使用这种线程池一定要流量是开发者知道的,最高峰时候的流量也不会导致CPU负载过高,才能使用这种线程池。

  • 任务调度线程池:ScheduledThreadPoolExecutor。

可以根据自己的需求,使用单线程调度(SingleThreadScheduledExecutor),多线程调度(ScheduledThreadPool)。不过现在使用spring调度比较多点,我自己在开发中使用线程池的调度也比较少了,基本会使用spring的调度。

  • 自定义线程池(推荐使用)

个人比较推荐这种方式,根据实际的一个业务场景,自己new一个ThreadPoolExecutor,参数根据业务场景需要指定合适的参数,比如核心线程数设置多少合适,最大线程数设置多少合适,任务队列设置多大的有界合适,拒绝策略也可以自定义,一般采用离线存储啥的,完全根绝自己的业务场景来定制。这样可以保证不会发生无界队列导致oom,也不会导致创建的线程过多而导致机器卡死(一般I5、4核的处理器跑1000左右的线程就会负载过高)。

4. 线程池关闭

  1. shutdown():优雅关闭。调用之后不允许提交新的任务了,所有调用之前提交的任务都会执行,等所有任务执行完了,才会真正关闭线程池,这就是优雅的关闭方式。
  2. shutdownNow():强制关闭。返回还没执行的task列表,然后不让等待的task执行,尝试停止正在执行的task,非优雅关闭,强制关闭。

四、 线程池在使用过程中存在的一些问题以及解决方案

  • 在一些存在流量高峰,一段时间内并发量很大,参数设置不当可能导致性能不佳,CPU负载过高,内存溢出,拒绝策略设置不当导致任务丢失或者执行失败等等问题。

这些问题开发者可以使用上面提高的自定义创建线程池自行根据业务场景来设置线程池的参数,从而规避上述的一些问题。

  • 服务重启导致内存的任务队列中的任务全部丢失。

这种情况,如果业务场景是需要保证消息的百分百不丢失,那就需要在提交任务时,对任务做离线存储,在任务执行完过后,再将对应的离线存储的任务删除。服务启动后,需要起一个后台线程去加载一次离线存储的任务,提交给线程池去执行。

你可能感兴趣的:(Java,SE)