线程池详解(建议收藏)

概念

线程池(Thread Pool)是一种基于池化技术的多线程处理形式,用于管理线程的创建和生命周期,以及提供一个用于并行执行任务的线程队列。线程池的主要目的是减少在创建和销毁线程时所花费的开销和资源,提高程序性能,同时也提供了对并发执行任务的更好管理,例如控制线程数量。

使用线程池的好处

  • 线程复用:线程池中的线程可以被重复利用,用于执行多个任务,避免了频繁创建和销毁线程的性能开销。提高响应速度
  • 资源控制:线程池可以限制系统中线程的最大数量,防止因为线程数过多而消耗过多内存,或者导致过高的上下文切换开销。
  • 更方便的管理:通过线程池提供了可配置的参数,如核心线程数、最大线程数、空闲线程存活时间、任务队列的大小等,允许定制以适应不同的应用需求

线程池七大参数详解

在这里插入图片描述

核心线程数

概念

这是线程池中始终保持的线程数量,即使它们处于空闲状态也不会被回收,这样保证当新任务到来时能够快速进行响应,核心线程数设置的太小会导致用户请求响应不及时,核心线程数设置的太大会导致线程资源浪费,下文会讲解如何设计核心线程数

任务队列

概念

线程池中的任务队列是一个非常重要的组件,它用于存储等待被线程池中的线程执行的任务,我们上文中提到了,核心线程数不宜设置过多,否则会造成线程资源浪费和cpu上下文频繁切换的问题,核心线程能够满足系统平均流量即可,所以当某些特殊情况下系统流量激增的时候
当所有核心线程都忙于处理任务时,那么新任务将被放入任务队列中排队等待

评估任务队列的大小

任务队列的大小,可以设计为核心线程每秒所能处理的任务数的 2倍即可,比如我们上面案例中计算出了核心线程数为4,每秒可以处理20个请求,那么任务队列的大小设置为40即可

任务队列类型

SynchronousQueue
它与其他阻塞队列不同的地方在于,它内部并不持有任何数据元素,也就是说它没有任何内部容量(容量为0)。SynchronousQueue 的工作方式更像是一个传递手棒的机制,它适用于将任务直接从生产者传递给消费者的场景。

例如我们可以这么定义线程池

private final ExecutorService executor = new ThreadPoolExecutor(0, 200,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(), Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy());

适用场景

  • 适用于需要快速处理的高并发短任务。对实时性要求高的任务处理场景。比如支付场景、实时聊天场景
  • 适用于无核心线程的场景,比如实时监控报警系统,监控系统需要对异常事件进行实时处理和报警。当事件发生时,系统需要迅速处理并生成报警信息,但在没有事件发生时,不需要占用过多的系统资源。

LinkedBlockingQueue
LinkedBlockingQueue默认构造器参数的容量大小为Integer.MAX_VALUE,可就是说可看做是无穷大的容量

private static final ExecutorService executor = new ThreadPoolExecutor(32,
            32, 60000L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(),
            new CommonThreadFactory(),
            new ThreadPoolExecutor.DiscardOldestPolicy());

用它来做任务队列几乎不会被填满,因此通常不会触发拒绝策略,线程池的非核心线程(即大于核心线程数的线程)只有在任务队列已满的情况下才会被创建。由于任务队列几乎不会被填满,因此非核心线程几乎不会被创建。即无法创建非核心线程去处理任务,也无法抛出拒绝策略,如果任务提交速率高于任务处理速率,会有越来越多的任务对象堆积在这个阻塞队列中,造成较大的内存压力甚至导致OOM,此外,如果是我们上线或者重启服务,阻塞队列中任务会丢失,需要采取额外策略去解决阻塞队列任务丢失风险

如果我的线程池任务都是不可丢弃的,且我不想使用callerRunPolicy,因为我的线程任务是纯异步的,我不想让把线程任务抛给主线程影响我的主业务流程,这种情况下我可以适当把阻塞队列设置的大一些,甚至是无限大的

最大线程数

概念

这是线程池可以同时运行的最大线程数量。当工作队列满了,且当前运行的线程数少于最大线程数时,线程池会创建新的线程来处理任务。需要注意,最大线程数中是包括核心线程数的,

计算最大线程数

最大线程数的比如遇到双11、618这种请求高峰期,可能QPS会达到500,根据这个计算得出,最大线程数的计算公式 = (系统最大请求任务数 - 任务队列任务数)

拒绝策略

AbortPolicy

这个策略会直接抛出一个RejectedExecutionException异常,从而拒绝新任务的执行。这是默认的策略,这种策略也算是丢弃了任务任务,只不过相比DiscardPolicy,AbortPolicy通过抛出异常显示的拒绝任务,这样在任务调用的地方,开发者可以通过异常捕获来做日志打印、利用递归重新提交任务等操作,但需要注意,高并发情况下可能同一个任务再重试的时候会频繁触发拒绝策略导致不断重试,这样的话会造成严重性能损耗,所以采取两个策略①等待重试,即让线程sleep一段时间再进行重试②限制重试次数,案例代码如下

public class RetryTaskExample {

    public static void main(String[] args) {
        // 创建线程池
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                2, // 核心线程数
                4, // 最大线程数
                10, // 空闲线程的存活时间
                TimeUnit.SECONDS, // 时间单位
                new ArrayBlockingQueue<>(2), // 任务队列
                new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
        );

        // 提交任务
        for (int i = 1; i <= 6; i++) {
            final int taskId = i;
            submitTask(executor, taskId, 0);
        }

        // 关闭线程池
        executor.shutdown();
    }

    private static void submitTask(ThreadPoolExecutor executor, int taskId, int count) {
        try {
            executor.execute(() -> {
                System.out.println("Running task " + taskId + " by " + Thread.currentThread().getName());
                try {
                    // 模拟任务执行时间
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    System.out.println("Task " + taskId + " was interrupted.");
                }
            });
            System.out.println("Successfully submitted task " + taskId);
        } catch (RejectedExecutionException e) {
            // 日志记录
            System.out.println("Task " + taskId + " rejected: " + e.getMessage());
            System.out.println("Trying to resubmit task " + taskId);
            
            // 简单重试策略
            try {
                if (count < 3) {
                    // 等待一段时间后重试
                    TimeUnit.MILLISECONDS.sleep(500);
                    submitTask(executor, taskId, ++count); // 递归调用,重新提交任务
                }

            } catch (InterruptedException ex) {
                System.out.println("Retry of task " + taskId + " was interrupted.");
            }
        }
    }
}

DiscardPolicy

相比于这个策略将悄无声息地丢弃无法处理的任务,不会抛出异常,也不会给调用者任何提示。

CallerRunsPolicy

将任务回退给调用者线程(即提交任务的线程)来直接执行,这种方式会降低对线程池的新任务提交速度,因为提交任务的线程被占用来执行任务,但可以保证无论何种情况任务都会被提交并执行,虽然不是默认的策略,但确实实际开发中用的最多的策略。案例代码

    public static void main(String[] args) {
        // 创建线程池
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                2, // 核心线程数
                4, // 最大线程数
                10, // 空闲线程的存活时间
                TimeUnit.SECONDS, // 时间单位
                new ArrayBlockingQueue<>(2), // 任务队列
                new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
        );

        Long startTime = System.currentTimeMillis();
        // 通过for循环来模拟高并发的场景
        // 主线程通过调用方法submitTask来提交任务,线程池中的线程来执行任务,但在并发量过大,超出了线程池的
        for (int i = 1; i <= 10; i++) {
            final int taskId = i;
            submitTask(executor, taskId, 0);
        }

        // 关闭线程池
        executor.shutdown();
    }

日志打印为
在这里插入图片描述
由此可知,第1,2个任务由核心线程去执行,第3,4个线程存储在任务队列中,第5,6个线程由非核心线程去处理,第7,8个线程就会由main线程去处理,只有等main线程执行完毕后才会去提交并执行剩余的任务

我使用线程池的目的通常是用来做异步操作的,使用这个拒绝策略等价于说如果线程池中的线程都在忙碌中,我就不做异步了,直接把这部分业务代码放在主线程中去执行

DiscardOldestPolicy

这个策略将尝试丢弃队列中最早的一个任务,然后尝试再次提交新任务(不保证成功)。这种方式在任务可以被丢弃且希望运行最新任务时可能会有用。

如何设计线程池核心线程数目与最大线程数?

首先评估当前需要使用线程池的业务是属于cpu密集型还是io密集型

  • 如果cpu密集型,那么线程的数量不宜设置过多,因为如果线程数量过多,线程数最好不要超过cpu核心数,如果线程数远大于cpu核心数的话,那么会伴随大量的cpu上下文切换,浪费cpu时间,此外,多线程开发中我们经常会对一些共享资源加锁,线程数目过多会导致锁竞争加剧。
  • 如果是io密集型则相反,线程的数量需要多设置一些,线程数最好是cpu核心数的2倍以上,原因是io密集型环境中一个线程执行过程中往往伴随大量的io阻塞,我们所希望的是,当一个线程进入io阻塞状态时,它所持有的cpu资源被释放出来后会立马被其他线程抢占,这样可以确保cpu资源被最大化利用,所以这一切的前提就是要把线程数量设置的多一些,否则如果线程数量不够多的话,这些为数不多的线程进入阻塞后,cpu资源大部分都是空闲状态,很多用户请求阻塞在线程池排队队列中,但受限于线程数不够无法充分利用这些cpu资源,导致线程池用户请求排队队列中的请求越积越多

评估系统的qps

案例一:cpu密集型业务场景,我们主机的cpu核心数是24,系统稳定情况下平均流量为20QPS,每个请求执行时间为0.2秒,那么核心线程数应该设置为多少?

每个请求的执行时间为0.2秒,平均流量为20 QPS,那么每秒需要的计算资源是 20×0.2= 4个核心。所以我们的核心线程数设置为4个即可

案例二:cpu密集型业务场景,我们主机的cpu核心数是24,系统稳定情况下平均流量为200QPS,每个请求执行时间为0.2秒,那么核心线程数应该设置为多少?

每个请求的执行时间为0.2秒,平均流量为200 QPS,那么每秒需要的计算资源是 200×0.2= 40个核心。但我们的主机cpu核心数只有24,也就是说我们cpu的处理能力最大是每秒处理120个任务,也就是每秒都会堆积80个无法被处理的任务,除非是采用直接抛弃多余任务的拒绝策略,否则任务对象的数目越积越多可能会导致OOM(下文中会详细介绍这种情况),这已经不是增加线程数可以应对的了,而是要考虑给当前的服务集群加主机去分担流量了,比如再加一台cpu核心数为24的服务器

如何保证重启服务时,线程池的阻塞队列中排队中的任务不会丢失?

线程池的shutdown方法

调用 线程池的shutdown 方法,会优雅的关闭线程池,所谓的优雅就是不会立马粗暴的关闭线程池,虽然它不再接受新的任务,但会继续执行已经提交的任务,包括在队列中的任务

shutdownNow()方法用于立即关闭线程池。调用该方法后,线程池将尝试停止所有正在执行的任务,并返回等待执行的任务列表

@Slf4j
public class ThreadPoolShutdownExample {

    public static final ExecutorService CAMPAIGN_EXECUTOR_POOL =
            new ThreadPoolExecutor(
                    2,
                    2,
                    0L,
                    TimeUnit.MILLISECONDS,
                    new LinkedBlockingQueue<Runnable>(),
                    new DefaultThreadFactory("test"));

    public static void main(String[] args) {

        // 提交一些任务,这些任务一部分会在执行,另一部分则是会存入排队队列中
        for (int i = 0; i < 20; i++) {
            final int taskId = i;
            CAMPAIGN_EXECUTOR_POOL.submit(() -> {
                try {
                    System.out.println("Executing task " + taskId);
                    // 模拟任务执行时间
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    System.out.println("Task " + taskId + " was interrupted.");
                }
            });
        }

        // 调用 shutdown 方法,优雅的关闭线程池
        CAMPAIGN_EXECUTOR_POOL.shutdown(); 
        System.out.println("Shutdown initiated...");

        // 此时新来的任务线程池不会再接受了,并且抛出异常
        try {
            for (int i = 20; i < 40; i++) {
                final int taskId = i;
                CAMPAIGN_EXECUTOR_POOL.submit(() -> {
                    try {
                        System.out.println("Executing task " + taskId);
                        // 模拟任务执行时间
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        System.out.println("Task " + taskId + " was interrupted.");
                    }
                });
            }
        } catch (Exception e) {
            log.error("ThreadPoolShutdownExample catch exception:{}", e);
        }


        // 此处等待所有任务完成,超时设置为 5 秒,因为调用了shutdown方法需要等线程池所有剩余任务都被处理完才能顺利关闭线程,
        // 如果线程池中的排队任务较多5秒内无法处理完剩余任务,那么就调用shutdownNow,立刻强制关闭线程池,线程池剩余排队任务也丢弃掉
        try {
            if (!CAMPAIGN_EXECUTOR_POOL.awaitTermination(5, TimeUnit.SECONDS)) {
                System.out.println("Forcing shutdown...");
                CAMPAIGN_EXECUTOR_POOL.shutdownNow(); // 强制关闭
            }
        } catch (InterruptedException e) {
            System.err.println("Termination interrupted");
            CAMPAIGN_EXECUTOR_POOL.shutdownNow(); // 发生异常时强制关闭
        }

        System.out.println("Main thread exiting.");
    }
}

模拟线上环境解决方案

思路非常简单,使用一个ThreadPoolUtils统一管理我们系统所需要的全部线程池,使用@PreDestroy定义一个springboot服务关闭前自动调用的方法

@Component
public class ThreadPoolUtils {

    public static final ExecutorService CAMPAIGN_EXECUTOR_POOL =
            new ThreadPoolExecutor(
                    8,
                    8,
                    0L,
                    TimeUnit.MILLISECONDS,
                    new LinkedBlockingQueue<Runnable>(),
                    new DefaultThreadFactory("CAMPAIGN_EXECUTOR_POOL"));

    public static final ExecutorService FB_ASYNC_EXECUTOR = new ThreadPoolExecutor(
            2,
            10,
            0L,
            TimeUnit.MILLISECONDS,
            new SynchronousQueue<>(),
            new DefaultThreadFactory("FB_ASYNC_EXECUTOR"),
            new ThreadPoolExecutor.CallerRunsPolicy());

    public static final ExecutorService GG_ASYNC_EXECUTOR = new ThreadPoolExecutor(
            2,
            10,
            0L,
            TimeUnit.MILLISECONDS,
            new SynchronousQueue<>(),
            new DefaultThreadFactory("GG_ASYNC_EXECUTOR"),
            new ThreadPoolExecutor.CallerRunsPolicy());

    public static final ExecutorService MPA_ASYNC_SHARED_AUDIENCE_EXECUTOR = new ThreadPoolExecutor(
            4,
            10,
            0L,
            TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(300),
            new DefaultThreadFactory("MPA_ASYNC_SHARED_AUDIENCE_EXECUTOR"),
            new ThreadPoolExecutor.AbortPolicy());


    @PreDestroy
    public void shutdownExecutor() throws NoSuchMethodException {
        System.out.println("Shutting down the executor service...");
        Field[] fields = this.getClass().getDeclaredFields();
        for (Field field : fields) {
            if (ExecutorService.class.isAssignableFrom(field.getType())) {
                field.setAccessible(true);
                try {
                    ExecutorService executorService = (ExecutorService) field.get(this);
                    executorService.shutdown(); // Shutdown the executor service gracefully
                    try {
                        // 等待直到所有已经提交的任务完成,超时设置为 5 秒
                        if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) {
                            System.out.println("Forcing shutdown...");
                            executorService.shutdownNow(); // 强制关闭
                        }
                    } catch (InterruptedException e) {
                        System.err.println("Shutdown interrupted");
                        executorService.shutdownNow(); // 发生异常时强制关闭
                        Thread.currentThread().interrupt();
                    }
                    System.out.println("Executor service shutdown complete.");
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
    }

}

线程池的应用

框架中的应用

Web服务器

在处理HTTP请求时,每个请求都可以作为一个独立的任务提交到线程池中,由线程池中的线程处理,这样做的好处是可以快速响应用户请求,同时复用线程资源

开发中的常见场景

异步任务处理

例如发送电子邮件、执行后台计算等这些都可以作为异步任务提交给线程池,从而不会阻塞主程序的执行。

并行数据处理

比如我从mysql中去取出10万条数据,结果集是一个List,分批次处理,每个批次1000条,在for循环中循环的次数就是批次的数目,每次循环会提交给线程池一个futureTask异步运算任务,比如这里会分为100个批次,那么会for循环100次提交100个futureTask异步运算任务,线程池中的线程会并行去处理这些批次的数据,然后再把每个处理后的批次组合为一个最终结果

public class BatchDataProcessor {

    // 每批次处理的数据量
    private static int BATCH_SIZE = 5;
    // 创建一个固定大小的线程池
    private ExecutorService executorService = Executors.newFixedThreadPool(10);

    public void process(List<String> allDataList) {
        
        if (CollectionUtils.isEmpty(allDataList)) {
            return;
        }
        List<List<String>> batchDatas = Lists.partition(allDataList, BATCH_SIZE);
        // 用于存储所有的CompletableFuture
        List<CompletableFuture<List<String>>> futureList = new ArrayList<>();

        for (List<String> batchData : batchDatas) {
            // 分别为每个批次的数据创建一个CompletableFuture任务
            CompletableFuture<List<String>> future =
                    CompletableFuture.supplyAsync(() -> processData(batchData), executorService);
            // 将FutureTask添加到任务列表中
            futureList.add(future);
        }

        // 使用allOf方法,将所有的CompletableFuture组合为一个
        CompletableFuture<Void> allFutures = CompletableFuture.allOf(futureList.toArray(new CompletableFuture[0]));
        // 当所有的CompletableFuture都完成时,获取并处理所有批次的结果
        CompletableFuture<List<String>> allResultsFuture = allFutures.thenApply(v -> futureList.stream()
                .map(CompletableFuture::join)
                .flatMap(List::stream)
                .collect(Collectors.toList()));


        try {
            // 获取并处理所有批次的结果
            List<String> result = allResultsFuture.get();
            for (String s : result) {
                System.out.println(s);
            }
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }

        // 关闭线程池
        executorService.shutdown();

    }

    // 处理数据的逻辑
    private List<String> processData(List<String> batchData) {
        if (CollectionUtils.isEmpty(batchData)) {
            return batchData;
        }

        int index = 0;
        for (String batchDatum : batchData) {
            batchData.set(index, batchDatum + "had process");
            index++;
//            System.out.println(batchDatum + " " +Thread.currentThread().getName() + " had process");
        }

        // 模拟其它业务耗时
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        return batchData;
    }

    public static void main(String[] args) {
        BatchDataProcessor batchDataProcessor = new BatchDataProcessor();
        // 造数据
        List<String> allDataList = new ArrayList<>();
        for (int i = 0; i < 50; i++) {
            allDataList.add("data" + i);
        }

        batchDataProcessor.process(allDataList);
    }

}

延时异步任务

我需要开启一个异步任务,但这个异步任务需要等待30秒后再执行,最简单粗暴的方法是使用Thread.sleep方法,但这种的话会造成线程资源的浪费,高并发情况下就容易出现线程资源紧缺的问题,所以如何高效率实现这一需求呢?推荐使用ScheduledExecutorService,可以更加优雅地处理延迟任务。在指定的延迟时间后执行任务,而不会在此期间占用线程资源。

public class AsyncTaskExample {
    public static void main(String[] args) {
        ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);

        // 提交一个30秒后执行的任务
        executorService.schedule(() -> {
            // 这里编写任务完成后要执行的代码
            System.out.println("30秒已过,异步任务完成。");
        }, 30, TimeUnit.SECONDS);

        // 继续执行主线程的其他任务
        System.out.println("主线程继续执行...");

        // 关闭执行器服务 (根据需要选择适当的位置关闭)
        executorService.shutdown();
    }
}

### HTTP 401 Unauthorized 和无效授权令牌 当服务器接收到带有无效或缺失认证凭证的请求时,会返回 `401 Unauthorized` 状态码[^1]。这意味着客户端尝试访问受保护资源前必须提供有效的身份验证信息。 #### 原因分析 - 客户端未能向服务器发送任何认证信息。 - 提供的身份验证数据不被接受或者已经过期。 - 使用了错误类型的认证机制(例如,应该使用Bearer Token却用了Basic Auth)。 - 认证令牌本身存在问题,比如签名无效、范围不足或是已经被撤销。 #### 解决方案 针对不同场景下的具体措施: ##### 对于API调用者而言: 如果是在开发环境中遇到此问题,可以考虑以下方法来解决问题: - **确认Token的有效性和格式** 检查所使用的Token是否仍然有效以及其格式是否正确。确保按照预期的方式传递给Authorization头字段。通常情况下,对于OAuth2.0协议,应采用如下形式设置头部信息: ```bash Authorization: Bearer <access_token> ``` - **获取新的Access Token** 如果当前持有的Token已失效,则需重新发起一次认证流程以换取最新的AccessToken。这可能涉及到刷新旧有的RefreshToken或者是让用户再次输入用户名密码等敏感资料完成整个过程。 - **配置Postman工具** 当利用像Postman这样的第三方应用程序来进行调试工作时,务必保证该应用能够正确处理并附带必要的认证参数。参照官方文档说明调整环境变量设定,并将取得的合法Token填充到相应位置以便后续操作正常执行[^3]. ##### 微服务架构中的特殊考量 在基于微服务的应用程序里,特别是那些采用了Zuul作为网关组件的情况之下,可能会碰到额外的安全策略限制导致同样的现象发生。此时建议采取下面几种方式之一加以应对: - 修改Zuul路由规则使其兼容现有的安全框架; - 调整下游各子系统的权限控制逻辑使之更加宽松一些; - 或者干脆更换成其他更适合企业级需求的产品替代品[^2]. ```python import requests url = 'https://example.com/api/resource' headers = { 'Authorization': f'Bearer {valid_access_token}' } response = requests.get(url, headers=headers) if response.status_code == 401: print('Invalid or expired token.') else: data = response.json() ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值