ThreadPool线程池

线程频繁的创建和销毁会带来太多的调度开销,为了减少这种开销可以使用线程池技术。线程池内部能够维护多个线程随时分配,相比频繁创建销毁线程,使用线程池能够降低资源消耗、提高响应速度,此外还能够对线程数进行统一控制,避免耗尽系统资源。

线程池基本概念

这里我们先介绍一些线程池经常用到的概念。

线程池参数

corePoolSize:线程池的核心线程数

maximumPoolSize:线程池能容纳的最大线程数

keepAliveTime:空闲线程的存活时间

workQueue:存放提交但未执行的阻塞队列

threadFactory:创建线程的工厂类

handler:队列满后的拒绝策略

我们向线程池提交任务,当线程数小于corePoolSize时,线程池会立即创建线程;当提交任务数大于corePoolSize时,会优先将任务存放到workQueue;当队列饱和后会扩充线程池的线程数直到达到maximumPoolSize,此时再有多余的任务就会触发线程池的拒绝策略;当线程无事可做超过了keepAliveTime时,线程池的线程数就会逐步收缩直到达到corePoolSize

这里注意阻塞队列如果使用ArrayBlockingQueue是需要指定队列初始值的,但如果使用LinkedBlockingQueue且没有指定队列容量那么默认就没有最大值了(即最大值为Integer.MAX_VALUE),然而这可能导致队列任务积压过多,实际开发中十分不建议不指定队列容量。

线程池拒绝策略

对于拒绝策略,则有以下几种:

AbortPolicy:触发拒绝策略时,直接抛出RejectedExecutionException异常拒绝执行任务,这也是线程池默认的拒绝策略。

CallerRunsPolicy:触发拒绝策略时,只要线程池还没关闭就使用调用线程直接运行。这种策略适合并发量小、不允许失败的场景,如果并发量较大会导致主线程严重阻塞,性能损失较大。

DiscardPolicy:直接丢弃任务,没有其它操作。

DiscardOldestPolicy:触发拒绝策略时,丢弃阻塞队列中最老的任务,将新任务加入。

线程池使用例子

这里我们直接看一个使用线程池的例子。

package com.gacfox.demo;

import java.util.concurrent.*;

public class Main {
    public static void main(String[] args) {
        // 创建线程池
        ExecutorService executorService = new ThreadPoolExecutor(
                3,
                3,
                60L, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(10),
                Executors.defaultThreadFactory()
        );
        // 使用线程池调度10个任务
        for (int i = 0; i < 10; i++) {
            executorService.execute(() -> {
                try {
                    Thread.sleep(1000);
                    System.out.println("线程" + Thread.currentThread().getId() + "执行完毕");
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            });
        }
    }
}

代码中,我们创建了一个大小为3的线程池,使用容量为10的ArrayBlockingQueue作为任务队列,之后我们用这个线程池调度10个任务。

注:上面代码线程池没有关闭,因此程序运行完成后也不会退出。实际开发中,如果我们需要关闭线程池,可以调用线程池的shutdown()方法。

Executors创建线程池

JDK中的Excutors为我们提供了几种默认的线程池实现。

线程池 说明
newCachedThreadPool 可缓存线程池,当线程池中线程超过空闲时间时回收,当任务需要线程超过线程池线程数时会新建线程
newFixedThreadPool 固定大小线程池,任务需要的线程超出线程池大小时会阻塞等待
newScheduleThreadPool 固定大小线程池,支持定时和周期性执行任务
newSingleThreadExecutor 单线程化的线程池,它能保证所有任务按照FIFO、LIFO或优先级顺序执行

下面是一个例子。

package com.gacfox.demo;

import java.util.concurrent.*;

public class Main {
    public static void main(String[] args) {
        // 创建线程池
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        // 使用线程池调度10个任务
        for (int i = 0; i < 10; i++) {
            executorService.execute(() -> {
                try {
                    Thread.sleep(1000);
                    System.out.println("线程" + Thread.currentThread().getId() + "执行完毕");
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            });
        }
    }
}

代码中,我们直接用Executors创建了一个默认的定长线程池,并用其调度10个任务。

不过实际使用中,我们发现这些JDK默认的线程池实现都没有队列容量参数,前面说过指定一个合适的队列容量能够避免系统任务积压,因此这个参数还是比较重要的,JDK实现的这几个线程池却无法设置该参数,这就比较尴尬了,因此不建议使用这些线程池实现,还是建议使用new ThreadPoolExecutor()的方式创建线程池。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。
Copyright © 2017-2024 Gacfox All Rights Reserved.
Build with NextJS | Sitemap