聊聊Java线程池原理

2020-12-26  本文已影响0人  平凡的柚子

线程池是很常用的并发框架,几乎所有需要异步和并发处理任务的程序都可用到线程池。
使用线程池的好处如下

  1. 降低资源消耗:可重复利用已创建的线程池,降低创建和销毁带来的消耗;
  2. 提高响应速度:任务到达时,可立即执行,无需等待线程创建;
  3. 提高线程的可管理性:线程池可对线程统一分配、调优和监控。

原理

线程池的原理非常简单,这里用处理流程来概括:

  1. 线程池判断核心池里的线程是否都在执行任务,如果不是,创建一个新的线程来执行任务;
  2. 如果核心线程池已满,则将新任务存在工作队列中;
  3. 如果工作队列满了,线程数量没有达到线程池上限的前提下,新建一个线程来执行任务;
  4. 线程数量达到上限,则触发饱和策略来处理这个任务;

使用工作队列,是为了尽可能降低线程创建的开销。工作队列用阻塞队列来实现。

阻塞队列

阻塞队列(BlockingQueue)是指支持阻塞的插入和移除元素的队列。

原理:使用通知者模式实现。当生产者往满的队列中添加元素时,会阻塞生产者。消费者移除元素时,会通知生产者当前队列可用。

阻塞队列有以下三种类型,分别是:

有界阻塞队列

主要包括ArrayBlockingQueue(数组),LinkedBlockingQueue(链表)两种。有界队列大小与线程数量大小相互配合,队列容量大线程数量小时,可减少上下文切换降低cpu使用率,但是会降低吞吐量。

无界阻塞队列

比较常用的是LinkedTransferQueue。FixedThreadPool就是用这个实现的。无界阻塞队列要慎重使用,因为在某些情况,可能会导致大量的任务堆积到队列中,导致内存飙升。

同步移交队列

SynchronousQueue。不存储元素的阻塞队列,每一个put操作必须等待一个take操作,否则不能继续添加元素。用于实现CachedThreadPool线程池。

各个线程池所使用的任务队列映射关系如下:

线程池 阻塞队列
FixedThreadPool LinkedBlockingQueue
SingleThreadExecutor LinkedBlockingQueue
CachedThreadExecutor SynchronousQueue
ScheduledThreadPoolExecutor LinkedBlockingQueue

实现类分析

ThreadPoolExecutor是Java线程池的实现类,是Executor接口派生出来的最核心的类。依赖关系图如下:

这里不得不提到Executor框架,该框架包含三大部分,如下:

线程池创建

new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, milliseconds, runnableTaskQueue, handler)

构造方法如下:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

参数说明:

使用Executors创建线程池

使用工具类Executors可创建三种类型的线程池:FixedThreadPool、SingleThreadExecutor、CachedThreadPool。本质上也是调用上述构造方法。理解了前文的参数解释,下面三种线程池也就容易理解了。

可重用固定线程数的线程池。

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

工作流程如下:

  1. 如果当前运行的线程数少于corePoolSize,则创建新线程来执行任务;
  2. 线程数等于corePoolSize之后,新任务加入LinkedBlockingQueue(无界阻塞队列)。因为最大线程数maximumPoolSize参数值等于corePoolSize,不会产生多余线程;
  3. 线程执行完任务之后会反复从LinkedBlockingQueue中获取任务来执行。

单个worker线程的线程池

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

SingleThreadExecutor与FixedThreadPool的区别在于,maximumPoolSize和corePoolSize都设置成了1,其它参数都一样。

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

CachedThreadPool将corePoolSize设置为0,maximumPoolSize设置为无限大,同时使用了一个没有容量的工作队列SynchronousQueue。这个线程池没有固定的核心线程,而是根据需要创建新线程。

工作流程:

  1. 有新任务时,主线程执行SynchronousQueue.offer操作,空闲线程执行SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)操作,配对成功则将任务交给空闲线程执行;
  2. 当没有空闲线程时,上面的配对操作失败,此时会创建一个新线程来执行任务;
  3. 任务执行完毕后,空闲线程会等待60秒。60秒内如果有新任务,就立即执行,否则时间一过线程就终止。

线程池关闭

调用shutdown或者shutdownNow方法可关闭线程池。原理是遍历线程池中所有工作线程,调用interrupt方法来中断线程。

这里涉及到ThreadPoolExecutor中定义的线程的五种状态

// runState is stored in the high-order bits
private static final int RUNNING    = -1 << COUNT_BITS;
private static final int SHUTDOWN   =  0 << COUNT_BITS;
private static final int STOP       =  1 << COUNT_BITS;
private static final int TIDYING    =  2 << COUNT_BITS;
private static final int TERMINATED =  3 << COUNT_BITS;

当队列和正在执行的任务都为空时,由SHUTDOWN转化为TIDYING;当正在执行的任务为空,由STOP转化为TIDYING。

本博客从线程池的原理介绍作为切入点,分析了线程池中尤为关键的组件:阻塞队列。同时分析了线程池的核心实现类ThreadPoolExecutor。以线程池的创建和关闭的思路,梳理了相关知识点,包括三种常用线程池介绍以及线程池五种状态。


最新2020整理收集的一些高频面试题(都整理成文档),有很多干货,包含mysql,netty,spring,线程,spring cloud、jvm、源码、算法等详细讲解,也有详细的学习规划图,面试题整理等,需要获取这些内容的朋友请加Q君样:11604713672
上一篇 下一篇

猜你喜欢

热点阅读