这下我懂线程池了吧!

682 阅读8分钟

这次我真的忘不了你了

献上标准线程池Api

public ThreadPoolExecutor(int corePoolSize,//核心数
                              int maximumPoolSize,//最大线程数
                              long keepAliveTime,//空闲时间
                              TimeUnit unit,//单位
                              BlockingQueue<Runnable> workQueue,//阻塞队列
                              ThreadFactory threadFactory,//线程工厂
                              RejectedExecutionHandler handler//拒绝策略
                              ) {//省略
}

整体模型

线程池在内部实际上构建了一个生产者消费者模型,将线程和任务两者解耦,并不直接关联,从而良好的缓冲任务,复用线程。

线程池的运行主要分成两部分:

  • 任务管理
    • 充当生产者的角色,当任务提交后,线程池会判断该任务后续的三种走向
      1. 直接申请线程执行该任务(如果线程数workerCount小于核心数)
      2. 缓冲到队列中等待线程执行(如果)
      3. 拒绝该任务
  • 线程管理
    • 线程管理部分是消费者,它们被统一维护在线程池内,根据任务请求进行线程的分配,当线程执行完任务后则会继续获取新的任务去执行,最终当线程获取不到任务的时候,线程就会被回收。

生命周期

ThreadPoolExecutor的运行状态有5种,分别为:

其生命周期转换如下入所示:

线程池内部使用一个变量维护两个值:运行状态(runState)和线程池内有效线程的数量 (workerCount)

用一个变量去存储两个值,可避免在做相关决策时,出现不一致的情况,不必为了维护两者的一致,而占用锁资源。

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

线程池的本质是对任务和线程的管理,而做到这一点最关键的思想就是将任务和线程两者解耦,不让两者直接关联,才可以做后续的分配工作。线程池中是以生产者消费者模式,通过一个阻塞队列来实现的。阻塞队列缓存任务,工作线程从阻塞队列中获取任务。

任务执行机制

当我们execute方法执行时,任务是怎么被调度的呢?

任务调度

  1. 首先检测线程池运行状态,如果不是RUNNING,则直接拒绝,线程池要保证在RUNNING的状态下执行任务。
  2. 如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务。
  3. 如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。
  4. 如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务。
  5. 如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常。

执行流程如下图所示:

这里重点是,无界队列由于阻塞队列一般都不会满,所以不会走到“线程数小于最大线程数”这一步的判断,导致maximumPoolSize参数无效。

阻塞队列

重要概念

Worker

线程池为了掌握线程的状态并维护线程的生命周期,设计了线程池内的工作线程Worker

线程池执行线程的基本元素,是一个Runnable

增加线程 addWorker

增加线程是通过线程池中的addWorker方法

addWorker方法有两个参数:firstTask、core

  • firstTask参数用于指定新增的线程执行的第一个任务,该参数可以为空

  • core参数为true表示在新增线程时会判断当前活动线程数是否少于corePoolSize,false表示新增线程前需要判断当前活动线程数是否少于maximumPoolSize

重写Runnable run()

在Worker类中的run方法调用了runWorker方法来执行任务

public void run() {
            runWorker(this);
}
final void runWorker(Worker w) {
        try {
            while (task != null || (task = getTask()) != null) {
                //省略执行代码
            }
        } finally {
            processWorkerExit(w, completedAbruptly);
        }
}

执行过程如下:

  1. while循环不断地通过getTask()方法获取任务。
  2. 执行任务
  3. 如果getTask结果为null则跳出循环,执行processWorkerExit()方法,销毁线程。

getTask()

从阻塞队列中取任务

主要有两个重点

  1. 当前线程池状态的值是SHUTDOWN或以上时,不允许再向阻塞队列中添加任务
  2. 核心线程可以无限等待获取任务,对于超过核心线程数量的这些线程,需要进行超时控制。目的是控制线程池的有效线程数量
private Runnable getTask() {
        boolean timedOut = false; // Did the last poll() time out?
        for (;;) {
            //第一个重点
            if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
                decrementWorkerCount();
                return null;
            }
            // 对于超过核心线程数量的这些线程,需要进行超时控制
        	boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
        }
    }

Worker线程回收processWorkerExit()

当Worker无法获取到任务,也就是获取的任务为空时,循环会结束,Worker会主动消除自身在线程池内的引用。

线程池需要管理线程的生命周期,需要在线程长时间不运行的时候进行回收。线程池使用一张Hash表去持有线程的引用,这样可以通过添加引用、移除引用这样的操作来控制线程的生命周期。

AQS在线程池的应用

Worker继承自AQS,用于判断线程是否空闲以及是否可以被中断。

线程回收时如何判断线程是否在运行?

Worker是通过继承AQS,使用AQS来实现独占锁这个功能。没有使用可重入锁ReentrantLock,而是使用AQS,为的就是实现不可重入的特性去反应线程现在的执行状态。

  1. lock方法一旦获取了独占锁,表示当前线程正在执行任务中。
  2. 如果正在执行任务,则不应该中断线程。
  3. 如果该线程现在不是独占锁的状态,也就是空闲的状态,说明它没有在处理任务,这时可以对该线程进行中断。
  4. 线程池在执行shutdown方法或tryTerminate方法时会调用interruptIdleWorkers方法来中断空闲的线程,interruptIdleWorkers方法会使用tryLock方法来判断线程池中的线程是否是空闲状态;如果线程是空闲状态则可以安全回收。

线程池都有哪几种工作队列

  • ArrayBlockingQueue

    一个用数组实现的有界阻塞队列,按FIFO排序量

  • LinkedBlockingQueue

    基于链表结构的阻塞队列,按FIFO排序任务,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE

    吞吐量通常要高于ArrayBlockingQuene

  • DelayQueue

    支持延时获取元素无界阻塞队列,队列使用PriorityQueue(非concurrent包)来实现

  • PriorityBlockingQueue

    一个支持优先级的无界阻塞队列。默认情况下元素采取自然顺序升序排列。也可以自定义类实现compareTo()方法来指定元素排序规则,或者初始化PriorityBlockingQueue时,指定构造参数Comparator来对元素进行排序

  • SynchronousQueue

    (同步队列)一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQuene

    重点 : offer方法如果里面有没被消费的直接返回false,put方法会阻塞知道被消费

    newCachedThreadPool线程池使用了这个队列

拒绝策略

  • AbortPolicy直接抛出异常阻止线程运行(默认)
  • DiscardOldestPolicy移除队列最早线程尝试提交当前任务
  • DiscardPolicy丢弃当前任务,不做处理
  • CallerRunsPolicy如果被丢弃的线程任务未关闭,由提交这个任务的线程执行该任务

JDK提供的几种线程池

Executors.newCachedThreadPool()

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

设想一下,如果SynchronousQueue换成LinkedBlockingQueue,workerCountOf(recheck) == 0也会addWorker-非核心的worker,那就会永远一直放到队列中,一直被一个线程消费(这里1个coreThread和0个core是一样的)

线程池execute尝试将任务放到队列中调用SynchronousQueue.offer时,offer方法如果里面有没被消费的直接返回false,所以直接会走到addWorker-非核心的worker,所以跟无界队列不一样,他会有N个线程消费

newFixedThreadPool

public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>(),
                                      threadFactory);
    }
  • 核心线程数和最大线程数大小一样
  • 没有所谓的非空闲时间,即keepAliveTime为0
  • 阻塞队列为无界队列LinkedBlockingQueue

newSingleThreadExecutor

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

一个人(一条线程)夜以继日地干活。

newScheduledThreadPool

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

DelayedWorkQueue :和DelayedQueue相似,但是是自己实现的

工作机制
  • 添加一个任务
  • 线程池中的线程从 DelayedWorkQueue 中取任务
  • 线程从 DelayedWorkQueue 中获取 time 大于等于当前时间的task
  • 执行完后修改这个 task 的 time 为下次被执行的时间
  • 这个 task 放回DelayedWorkQueue 队列中

一些问题

线程池怎么判断线程可以回收?

线程池需要管理线程的生命周期,需要在线程长时间不运行的时候进行回收。线程池使用一张Hash表(HashSet<ThreadPoolExecutor.Worker> workers)去持有线程的引用,这样可以通过添加引用、移除引用这样的操作来控制线程的生命周期。重要的就是如何判断线程是否在运行?答案是,每次runWorker都会调用AQS的lock()锁起来,当worker getTask为null时,开始尝试回收,此时tryLock,如果成功说明空闲是空闲状态则可以安全回收

待加