首页 > 其他分享 >线程池原理

线程池原理

时间:2023-06-13 14:13:51浏览次数:36  
标签:task Worker 任务 线程 workQueue 原理 null

下面我将围绕这几个问题,来讨论一下线程池。

  1. 线程池是什么?
  2. 为什么使用线程池,或者说使用线程池的好处是什么?
  3. 线程池怎么使用?
  4. 线程池的原理是什么,它怎么做到重复利用线程的?

1. 线程池是什么

线程池(Thread Pool)是一种基于池化思想的管理线程的工具,它内部维护了多个线程,目的是能重复利用线程,控制并发量,降低线程创建及销毁的资源消耗,提升程序稳定性。

2. 为什么用线程池

使用线程池的好处:

  1. 降低资源消耗:重复利用已创建的线程,降低线程创建和销毁造成的损耗。
  2. 提高响应速度:任务到达时,无需等待线程创建即可立即执行。
  3. 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。

线程池解决的核心问题就是资源管理问题,在并发场景下,系统不能够确定在任意时刻,有多少任务需要执行,有多少资源需要投入。这种不确定性将带来以下若干问题:

  1. 频繁申请/销毁资源和调度资源,将带来额外的消耗,可能非常巨大。
  2. 对资源无限申请缺少抑制手段,易引发系统资源耗尽的风险。
  3. 系统无法合理管理内部的资源分布,会降低系统的稳定性。

线程池这种基于池化思想的技术就是为了解决这类问题。

3. 线程池怎么用

线程池的的核心实现类是ThreadPoolExecutor,调用execute或者submit方法即可开启一个子任务。

public class ThreadPoolTest {

    private static ThreadPoolExecutor poolExecutor =
            new ThreadPoolExecutor(1, 1, 5, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1));

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Runnable runnableTask = () -> System.out.println("runnable task end");
        poolExecutor.execute(runnableTask);

        Callable<String> callableTask = () -> "callable task end";
        Future<String> future = poolExecutor.submit(callableTask);
        System.out.println(future.get());
    }
}

ThreadPoolExecutor的核心构造器有7个参数,我们来分析一下每个参数的含义:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
	// 省略...
}
  • corePoolSize:线程池的核心线程数。线程池中的线程数小于corePoolSize时,直接创建新的线程来执行任务。
  • workQueue:阻塞队列。当线程池中的线程数超过corePoolSize,新任务会被放到队列中,等待执行。
  • maximumPoolSize:线程池的最大线程数量。
  • keepAliveTime:非核心线程空闲时的存活时间。非核心线程即workQueue满了之后,再提交任务时创建的线程。非核心线程如果空闲了,超过keepAliveTime后会被回收。
  • unitkeepAliveTime的时间单位。
  • threadFactory:创建线程的工厂。默认的线程工厂会把提交的任务包装成一个新的任务。
  • handler:拒绝策略。当线程池的workQueue已满且线程数达到最大线程数时,新提交的任务执行对应的拒绝策略。

JDK也提供了一个快速创建线程池的工具类Executors,它提供了多种创建线程池的方法,但通常不建议使用Executors来创建线程池,因为它提供的很多工具方法,要么使用的阻塞队列没有设置边界,要么是没有设置最大线程的上限。任务一多容易发生OOM。实际开发应该根据业务自定义线程池。

4. 线程池原理

4.1. execute

线程池的核心运行机制在于execute方法,所有的任务调度都是通过execute方法完成的。

public void execute(Runnable command) {
    // ...
    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) { // (1)
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
	if (isRunning(c) && workQueue.offer(command)) { // (2)
        int recheck = ctl.get();
        // 重新检查状态,如果是非运行状态,接着执行队列删除操作,然后执行拒绝策略
        if (! isRunning(recheck) && remove(command))
            reject(command);
        // 如果是因为remove(command)删除队列元素失败,再判断池中线程数量
        // 如果池中线程数为0则新增一个任务为null的非核心线程
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    else if (!addWorker(command, false)) // (3)
        reject(command);
}

透过execute方法的3个if判断,可以把它的逻辑梳理为3个部分:

  1. 第一个if:如果线程数量小于核心线程数,则创建一个线程来执行新提交的任务。
  2. 第二个if:如果线程数量大于等于核心线程数,则将任务添加到该阻塞队列中。
  3. else if:线程池状态不对,或者添加到队列失败即队列满了,则创建一个非核心线程执行新提交的任务。如果非核心线程创建失败就执行拒绝策略。

4.2. addWorker

execute中的核心逻辑要看addWoker方法,它承担了核心线程和非核心线程的创建。addWorker方法前半部分代码用一个双重for循环确保线程池状态正确,后半部分的逻辑是创建一个线程对象Worker,添加到存储线程对象的HashSet中,然后使用Worker线程执行任务的过程。

Worker是对提交进来的线程的封装,创建的worker会被添加到一个HashSet,线程池中的线程都维护在这个名为workersHashSet中并被线程池所管理。

前面说到,Worker本身也是一个线程对象,它实现了Runnable接口,在addWorker中会启动一个新的任务,所以我们要看它的run方法,而run方法的核心逻辑是runWorker方法。

final void runWorker(Worker w) {
    // ...
    try {
        while (task != null || (task = getTask()) != null) {
            // ...
            try {
                try {
                    task.run(); // 执行普通的run方法
                } finally {
                    task = null; // task置空
                }
            }
        }
    } finally {
        processWorkerExit(w, completedAbruptly); // 回收空闲线程
    }
}

可以看到runWorker方法中有一个while循环,循环执行taskrun()方法,这里的task就是提交到线程池的任务,它对当成了普通的对象,执行完task.run(),最后会把task设置为null

再看循环的条件,已知task是有可能为空的,所以我们再看看(task = getTask()) != null这个条件,如果getTask() == null则跳出循环执行processWorkerExit方法,processWorkerExit方法的作用是回收空闲线程。

4.3. getTask

很多答案都在getTask()方法中。

private Runnable getTask() {
    boolean timedOut = false; // Did the last poll() time out?

    for (; ; ) { // (1)
        // 校验线程池状态的代码,先省略...
        
        int wc = workerCountOf(c);

        // Are workers subject to culling?
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize; // (2)

        if ((wc > maximumPoolSize || (timed && timedOut))
                && (wc > 1 || workQueue.isEmpty())) {
            if (compareAndDecrementWorkerCount(c)) // 线程数减1
                return null; // 这里时中断外层while循环的时机
            continue;
        }

        try {
            Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take(); // (3)
            if (r != null)
                return r; // 取到值了就在外层的while循环中执行任务
            timedOut = true; // 否则就标记为获取队列任务超时
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}

结合(1)、(3)这两个地方可以看出,getTask()方法是一个无限循环,不断从阻塞队列中取任务,取到了任务就返回,到外层runWorker方法中,执行这个任务的run方法。即线程池通过启动一个Worker子线程来执行提交进来的任务,并且一个Worker线程会执行多个任务

我们再看看getTask()何时返回null,因为返回null才可以看下一步的processWorkerExit方法。

getTask()返回null主要看timed && timedOut这个条件。变量值timedtrue的条件是:允许核心线程超时或者线程数大于核心线程数。timedOut变量为true的条件是从workQueue为空了,取不到任务了,但是这个前提是timed == true,执行workQueue.poll的时候,因为workQueue.poll方法获取任务最多等待keepAliveTime的时间,超过这个时间获取不到就返回null,而workQueue.take()方法获取不到任务会一直等待!

因此,在核心线程不会超时的情况下,如果池中的线程数小于核心线程数,这个getTask()会一直循环下去,这就是在这种情况下线程池不会自动关闭的原因!反之,在核心线程不会超时的情况下,如果池中的线程数超过核心线程数,才会对多余的线程回收。如果allowCoreThreadTimeOut == true,即核心线程也能超时,当阻塞队列为空,所有Worker线程都会被回收。

ThreadPoolExecutor的注释说,当池中没有剩余线程,线程池会自动关闭。

A pool that is no longer referenced in a program AND has no remaining threads will be shutdown automatically

但我也没找到证据,没看到哪里显式调用shutdown(),但确实会自动关闭。

4.4. processWorkerExit

getTask()获取不到任务后,会执行processWorkerExit方法回收线程。在这里,Worker线程集合随机删除一个线程对象,然后再随机中断一个workers中的线程。可见线程销毁线程的方式时删除线程引用,让JVM自动回收。

private void processWorkerExit(Worker w, boolean completedAbruptly) {
    // ...
    try {
        workers.remove(w);
    }
    // 调用interrupt()方法中断线程,一次中断一个
    tryTerminate();
    // ...
}

5. 线程池原理总结

最后我们回到最初的问题,线程池的原理是什么,线程池怎么做到重复利用线程的?

线程池通过维护一组叫Worker的线程对象来处理任务。在线程数不超过核心线程数的情况下,一个任务对应一个Worker线程,超过核心线程数,新的任务会提交到阻塞队列。一个Worker线程在启动后,除了执行第一次任务之外,还会不断从阻塞队列中消费任务。如果队列里没任务了,Worker线程会一直轮询,不会退出;只有在池中线程数超过核心线程数时才退出轮询,然后回收多余的空闲线程。即一个Worker线程会处理多个任务,且Worker线程受线程池管理,不会随意回收。

6. 线程池拒绝策略

拒绝策略的目的是保护线程池,避免无节制新增任务。JDK使用RejectedExecutionHandler接口代表拒绝策略,并提供了4个实现类。线程池的默认拒绝策略是AbortPolicy,丢弃任务并抛出异常。实际开发中用户可以通过实现这个接口去定制拒绝策略。

标签:task,Worker,任务,线程,workQueue,原理,null
From: https://www.cnblogs.com/cloudrich/p/17477329.html

相关文章

  • 一个线程池拒绝策略引发的问题
    extends:严选库存稳定性治理系列:一个线程池拒绝策略引发的血案(qq.com),  虽然是我遇到的一个棘手的生产问题,但是我写出来之后,就是你的了。-why技术-博客园(cnblogs.com) 你好呀,是歪歪。前几天,就在大家还沉浸在等待春节到来的喜悦氛围的时候,在一个核心链路上的核心系......
  • 理解ABR及其工作原理
    翻译|Alex技术审校|赵军本文来自OTTVerse,作者为KrishnaRaoVijayanagar。ABREasyTech#007#ABR表示AdaptiveBitrate(自适应码率),它广泛地描述了这样一个过程:视频和音频的质量和码率会根据当前网络状况的波动而发生自适应变化,以确保网络传输流畅。ABR明显不同于CBR(ConstantBitr......
  • 进程和线程
    概念进程:是并发执行的程序在执行过程中分配和管理资源的基本单位,是一个动态概念,是竞争计算机系统资源的基本单位线程:是进程的一个执行单元,是进程内调度实体,比进程更小的独立运行的基本单位进程线程区别地址空间:线程共享本进程的地址空间和资源,而进程之间是独立的地址空间和资......
  • 01 卢京潮《自动控制原理》学习笔记转
    原文:https://zhuanlan.zhihu.com/p/262021993先上一份821的考试大纲,四年大学出来的应该都知道课本会将知识点分为重点、一般、掌握、熟练、理解、熟悉、了解等几个等级:正确理解自动控制原理课程中的有关概念。掌握结构图等效变换方法和梅森公式。能根据结构图熟练求取系统的传......
  • MOS管基础知识:轻松理解MOS管工作原理
    MOS管是一种利用电场效应来控制其电流大小的半导体三端器件,很多特性和应用方向都与三极管类似。这种器件不仅体积小、质量轻、耗电省、寿命长、而且还具有输入阻抗高、噪声低、热稳定性好、抗辐射能力强等优点,应用广泛,特别是在大规模的集成电路中。根据导电沟道的不同,MOS管可分为......
  • 小灰灰深度学习day9——多线程读取小批量数据(这里运行的时候报错了,目前还不会解决,
    在这里先把代码放上来importtorchimporttimeimportnumpyasnpimporttorchvisionfromtorch.utilsimportdatafromtorchvisionimporttransformsfromd2limporttorchasd2ld2l.use_svg_display()#利用svg显示图片importosos.environ["KMP_DUPLICATE_LIB_OK......
  • kafka工作原理
    1.工作流程以及文件存储机制​ kafka中的消息是以topic进行分类的,生产消费消息都是面向topic。​ topic是逻辑上的概念,partition分区是物理上的概念,每个分区对应一个log文件,该log文件存储的就是producer生产的log数据。producer生产的数据会追加到文件末端。消费者组中的每......
  • 关于进程、线程、协程的概念以及Java中的应用
    进程、线程、协程本文将从“操作系统”、“Java应用”上两个角度来探究这三者的区别。一、进程在我本人的疑惑中,我有以下3个问题。1.1为什么要引入进程?在“多道程序环境下”,允许多个程序并发执行,此时它们将失去封闭性,并具有间断性以及不可再现性的特征,因此需要引入进程的概念......
  • RC4算法原理 && IDA识别RC4算法
    RC4算法原理&&IDA识别RC4算法RC4简介&&对称密码介绍在密码学中,RC4是一种流加密算法,密钥长度可变。加解密使用相同的密钥,隶属于对称加密算法。流密码属于对称密码算法一种,基本特征是加解密双方使用一串与明文长度相同的密钥流,与明文流组合来进行加解密密钥流通常是由某一确......
  • Linux日志切割神器logrotate原理介绍和配置详解
    1、原理介绍create这也就是默认的方案,可以通过create命令配置文件的权限和属组设置;这个方案的思路是重命名原日志文件,创建新的日志文件。详细步骤如下:重命名正在输出日志文件,因为重命名只修改目录以及文件的名称,而进程操作文件使用的是inode,所以并不影响原程序继续输出日志......