并发之线程池

  1. 日常所说的“核心线程”、“非核心线程”是一个虚拟的概念,是为了方便描述而虚拟出来的概念
  2. 在代码中并没有标记哪些线程为“核心线程”或者“非核心线程”。所有线程都是一样的。

为什么使用线程池

  1. 减少线程的创建与销毁
  2. 减少cpu的资源切换。如果线程很多的情况下,cpu切换会很大消耗资源。线程池的存在可以有效的限定执行的线程数量。
  3. 线程池可以监控线程的执行状态,便于分析处理

线程池是如何实现的?

  在Java中,线程池中所谓的“线程”,其实就是一个静态内部类Worker,它是基于AQS实现的,并实现Runnable。存放在线程池的HashSet<Worker> workers成员变量中。
  而需要执行的任务,则放在BlockingQueue<Runnable> workQueue中。这样,整个线程实现的基础思想就是:从workerQueue中获取任务,放在worker中进行处理。

静态内部类Worker:
private final class Worker extends AbstractQueuedSynchronizer implements Runnable {......}

线程池的核心参数

  • corePoolSize 核心线程数量
  • maximunPoolSize 最大线程数量 ,一般 maximunPoolSize > corePoolSize
  • keepAliveTime 空闲线程的存活时间
  • 存活时间单位
  • workQueue 任务队列(共计5种,按照不同的业务场景使用不同的队列)
  • threadFactory 线程工场对象
  • RejectedExecutionHandler 拒绝策列

线程池的线程会在何时创建与销毁

线程池的执行顺序

image

创建

首先,线程池在创建之初并不会创建线程当任务来临后才会创建第一个线程,每次创建线程都是做出如下判断:

  1. 如果正在运行的线程数量 < corePoolSize,那么马上创建线程运行这个任务;
  2. 如果正在运行的线程数量 >= corePoolSize,那么将这个任务放入队列;
  3. 如果这时候队列满了,而且正在运行的线程数量 < maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
  4. 如果队列满了,而且正在运行的线程数量 >= 于maximumPoolSize,那么线程池会抛出异常RejectExecutionException。

销毁

  当空闲线程大于空闲存活时间,就会销毁掉。直到线程数量等于核心线程数。剩下的线程就是核心线程。

核心线程是否能销毁

  ThreadPoolExecutor中存在一个参数allowCoreThreadTimeOut,此参数表示是否允许核心线程超时,默认为 false,所以一般情况下,是不会销毁的。
  如果调整参数,是可以做到销毁核心线程的。

核心方法- getWork() 获取任务方法

当线程完成当前任务后,会进入死循环,不断的尝试获取任务。

  • 如果 生存时间 > keepAliveTime,会返回 null
  • 如果 生存时间 < keepAliveTime,会阻塞获取
// 线程池源码,获取任务
// 为分析而简化后的代码
private Runnable getTask() {
    boolean timedOut = false;
    for (;;) {
        int c = ctl.get();
        int wc = workerCountOf(c);

        // timed变量用于判断是否需要进行超时控制。
        // allowCoreThreadTimeOut默认是false,也就是核心线程不允许进行超时;
        // wc > corePoolSize,表示当前线程池中的线程数量大于核心线程数量;
        // 对于超过核心线程数量的这些线程,需要进行超时控制
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

        if (timed && timedOut) {
            // 如果需要进行超时控制,且上次从缓存队列中获取任务时发生了超时,那么尝试将workerCount减1,即当前活动线程数减1,
            // 如果减1成功,则返回null,这就意味着runWorker()方法中的while循环会被退出,其对应的线程就要销毁了,也就是线程池中少了一个线程了
            if (compareAndDecrementWorkerCount(c))
            return null;
            continue;
        }

        try {
            Runnable r = timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take();

            // 注意workQueue中的poll()方法与take()方法的区别
            //poll方式取任务的特点是从缓存队列中取任务,最长等待keepAliveTime的时长,取不到返回null
            //take方式取任务的特点是从缓存队列中取任务,若队列为空,则进入阻塞状态,直到能取出对象为止

            if (r != null) return r;
            timedOut = true;
            } catch (InterruptedException retry) {
                timedOut = false;
        }
    }
}

线程池的拒绝策略

在添加工作任务时,工作队列满后,会执行拒绝策略(都是静态内部类

  • AbortPolicy:直接抛出拒绝异常,默认项
  • CallerRunsPolicy:由线程池的调用者线程去执行多余的方法,串行执行。场景:所有任务都要执行。
  • DiscardOledestPolicy:放弃等待队列中最老的数据,迎接新的数据
  • DiscardPolicy:直接无视,不做任何处理。不添加队列,不报错,不做标识

逻辑:查看 ThreadPoolExcutor.java 文件中 public void execute(Runnable var1) {......} 方法

任务线程如何被回收(存活时间到了)?回收前需要做哪些判断?

  1. 如果线程是异常结束的,则将 workerCount 数量 -1
  2. 将任务线程的taskCount,加到 线程池的 taskCount 上
  3. 将 任务线程 从 HashSet中删除。此时,从线程池上看线程已删除
  4. tryTerminate方法的作用,线程凉了,需要查一下,是不是线程也凉了
  5. 如果因为任务线程结束,导致线程池没有线程。但还有任务,则需要创建一个非核心线程

最后,任务线程给被 GC 回收掉

回收的方式,就是run方法结束了,线程就销毁了。

小结

线程池的大流程

  1. 创建核心线程
  2. 任务放到阻塞队列中
  3. 创建非核心线程
  4. 执行拒接策略
    image
可以忽略的学习笔记
/**
 * 学习 ThreadPoolExecutor
 *
 * 1. 线程池的作用:
 *          1.节省线程创建与销毁的开销,
 *          2.反馈线程的执行状态,
 *          3.避免CPU在大量线程下的资源消耗
 * 2. 以下所有代码中 ctl 的使用是最多的,主要记录获取任务线程数量(workCount)与线程池状态(runState),ctl = rs | wc
 * 3. 线程池状态 5中状态
 *          1. Running 正常接受任务并处理排队的任务
 *          2. shutDown 不接受任务,但处理任务
 *          3. stop 不接受新任务,不处理排队的任务,并中断正在进行的任务
 *          4. tidying 运行线程数量0,队列任务数为0
 *          5. terminated 终止线程池
 * 4. 基本上,针对任务线程的操作,都要判断线程池的状态。大多是,run状态才会继续运行
 * 5. Worker 是本类中的内部类,实现Runnable,主要作用是承载任务与线程
 * 6. worker 中thread属性,是后面执行任务的线程
 * 7. worker 只会临时保存第一个任务
 * 8. worker 继承 AQS,可以实现非公平锁。worker.thread 与 aqs.thread 是相同的,但作用不同,一个用于启动运行,一个用于阻塞
 * 9. 线程池中不分核心线程与空闲线程,只会在创建任务时,存在一个校验
 * 10. 线程池初始化不会创建线程,当第一个任务来,才会开始创建第一个线程
 * 11. 线程池参数要求,核心线程 >=0,最大线程 >0。所以,最少会创建一个非核心线程。
 * 12. 线程池的启动操作:execute -> addWorker(command, true) -> workQueue.offer -> addWorker(null, false) -> reject
 * 13. 新任务来临,线程池的操作:(与上面步骤对应)
 *          1.创建核心线程去执行,
 *          2.核心线程满了,放到 workerqueue阻塞队列中,
 *          3.workerQueue满了,创建非核心线程去执行,
 *          4.非核心线程满了,执行拒绝策略(默认是异常抛出)
 * 14. 添加任务(addWorker)步骤:addWorker -> compareAndIncrementWorkerCount(工作线程数量+1)
 *          -> 创建任务 Worker w = new Worker(Runnable) -> ReentrantLock.lock -> workers.add(w) -> ReentrantLock.unlock
 *          -> worker.thread.start -> 调用内部类 worker.run() -> 调用外部类ThreaPoolExecutor.runWorker ->
 *          -> 执行第一个任务:worker.firstWorker.run() -> 后续任务的获取:getTask().run()
 *          -> completedTasks++ -> processWorkerExit 删除线程
 * 15. 线程池中worker的启动调用步骤:(与上面步骤对应)
 *          1. 通过worker.thread.start() 开始运行任务
 *          2. 内部调用执行worker.run()
 *          3. 内部调用执行 worker.firstTask.run() 执行第一个任务
 *          4. 后面的任务,通过调用执行 getTask()方法,在执行 run()
 * */
posted @ 2023-10-24 17:51  之士咖啡  阅读(5)  评论(0编辑  收藏  举报