Java线程池的使用与原理

161 阅读6分钟

创建线程池

首先看看线程池的构造方法:

public ThreadPoolExecutor(
    //线程池核心线程数最大值
    int corePoolSize,
    //线程池中的线程数量最大值
    int maximumPoolSize,
    //线程池中非核心线程空闲的存活时间大小
    long keepAliveTime,
    //上个参数的时间单位
    TimeUnit unit,
    //存放待运行任务的阻塞队列
    BlockingQueue<Runnable> workQueue,
    //创建线程的工厂,用来初始化线程(指定名称等)
    ThreadFactory threadFactory,
    //线程池饱和后的拒绝策略,在后面会讲解
    RejectedExecutionHandler handler
) 
  1. newSingleThreadExecutor()

线程数量始终为1,而队列是无界的,能保证所有的任务按照顺序执行

  public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>(),
                                    threadFactory));
    }
  1. newCachedThreadPool()

线程会被缓存(可以重用),当没有缓存的线程可以用的时候,就需要创建新的工作线程。线程运行完任务后,有60s的时间等待新的任务(等待到了就去执行,相当于这个线程被复用了),如果60s过了,则线程会被销毁。长时间闲置时,此线程池不会销毁什么资源。

   public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>(),
                                      threadFactory);
    }
  1. newFixedThreadPool()

线程池里,最多有nThreads数量的线程在运行,所一,当一个任务被提交,但是运行中的线程数量已经达到nThreads,就会被添加到队列中,直到那些运行中的线程有完成任务并退出了,这个任务才会被创建线程,补足nThreads。

  public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>(),
                                      threadFactory);
    }

此线程池,没有非空闲时间,即keepAliveTime为0,所以线程不会被复用,而是直接销毁。线程池能保证同时运行的线程数量是固定的(Fixed)。

  1. newScheduledThreadPool()

顾名思义,是一个可以调度(间隔性或者周期性)的线程池。

    public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }

以上的构造函数里,添加了DelayedWorkQueue作为队列,故可以进行调度。

线程池的执行,结合其中的工作队列来实现功能

下面将给出线程池基本的工作流程(一个最基础的自定义的线程池的能力):

当我们手动创建一个线程池的时候,例如 用以下参数构造:

new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>(),
                                      threadFactory);

着重看一下一个参数:60L,这个时间参数。构造方法里的解释是:

@param keepAliveTime when the number of threads is greater than
     *        the core, this is the maximum time that excess idle threads
     *        will wait for new tasks before terminating.

意思是当线程数大于内核数时,这是多余的空闲线程将在终止之前等待新任务的最长时间。

所以当这个参数被设置为0的时候,线程完成任务后,就不会是所谓的空闲,而是被直接销毁。

当这个参数被设置为60的时候,一个线程完成任务后,将进入60秒的空闲状态,这个时间以内,如果线程池指派这个线程任务,它会从空闲状态进入工作状态,如此往复。但是如果,60秒的时间内都没有新的任务被指派给这个空闲线程,那么它就会被真正的销毁。

这个机制可以帮助线程复用。

其次,有一个参数对线程池的能力有至关重要的作用:

BlockingQueue<Runnable> workQueue //在执行任务之前用于保留任务的队列

通过这个参数,注入一个实行不同策略的阻塞队列。

什么是阻塞队列?

在队列为空时,获取元素的线程会等待队列变为非空。而当队列满时,存储元素的线程会等待队列可用。

联想一下生产者-消费者机制就能理解阻塞队列的作用。

下面来看看,有哪些阻塞队列,它们都被用在了哪些线程池上,实现了什么样的期望:

  1. LinkedBlockingQueue 链表阻塞队列,按FIFO排序,

    newFixedThreadPool线程池使用了这个队列:

    在构造的时候,创建了一个无界的LinkedBlockingQueue,故线程池可以放入无限数量的任务,等着一个一个被消费。

    newSingleThreadExecutor线程池也用了这个队列:

    构造时创建了一个无界的LinkedBlockingQueue,线程池可以一直放入任务,没有限制,只不过能运行的线程只有一个而已。

  2. SynchronousQueue 无元素阻塞队列,每个插入必须等到另一个线程移除。用“配对”来形容会更合适,当一个线程进行插入的时候,必须有一个移除线程和它配对,拿走它插入的数据,否则就会一直阻塞。

    newCachedThreadPool线程池使用了这个队列:

    这个线程池核心的线程数量为0,而最大的线程数量为无穷,而有60s的空闲延迟时间,所以当一个任务进入时,必须要找到一条工作线程处理他,如果当前没有空闲的线程,那么就会再创建一条新的线程(非核心线程)。感觉有点像是没有线程池一样,但是有一个好处就是,线程不像是“野生”创建的那样执行完后就销毁,而是会有被保留的机会,被重用。而SynchronousQueue的作用只不过是在于让所有请求都得到回复。

  3. DelayQueue 延迟队列:支持延时获取元素的阻塞队列, 内部采用优先队列 PriorityQueue 存储元素。在创建元素时可以指定多久才可以从队列中获取当前元素,只有在延迟期满时才能从队列中提取元素。

    newScheduledThreadPool线程池使用了这个队列:

    容易理解,这个队列可以满足延迟的需求。

线程池的执行:

线程池的执行,不论是否为通过上面所述的方法创建的,还是手动使用构造方法创建的,都遵循以下的运行规则:

  1. 因为线程池区分核心线程,故一个任务被提交后,如果核心数量还没满,就创建核心线程并执行任务。
  2. 核心线程已满,如果线程池中的队列(不管是哪种队列)没有满,那就让任务进入队列。
  3. 如果队列也满了,那线程池会检查“是否当前整个线程池的线程数量已经超过了线程池的最大值”(线程池中的线程数量最大值 int maximumPoolSize),如果没有超过了话,线程池就立即创建一个非核心的线程并执行任务。
  4. 如果很不幸,以上几个条件都依次没有满足,那只能采取拒绝策略(RejectPolicy)了。

线程池的四种拒绝策略

  1. AbortPolicy(直接抛出一个异常)
  2. DiscardPolicy(丢弃任务,不做处理)
  3. DiscardOldestPolicy(丢弃队列里最老的任务,将当前这个任务继续提交给线程池)
  4. CallerRunsPolicy (直接由调用线程池的线程来显式执行了)

状态转换

线程池的状态:

  1. RUNNING 运行中,平常处理。

  2. SHUTDOWN 不接受新的任务 但继续处理队列的任务。

    当队列为空,并且线程池中执行的任务也为空,就会进入TIDYING状态。

  3. STOP 不接受新任务 不处理队列任务 甚至切断正在执行任务的线程。

    当线程池里的任务为空,就会进入TIDYING状态。

  4. TIDYING 任务为空了,会执行terminated方法。

  5. TERMINATED 执行了terminated方法后,会进入这个状态,线程池终结。