Java线程池理解与举例使用(详细)

小聊: 线程池的概念还是很重要的,“池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。线程池、数据库连接池、Http连接池等等都是对这个思想的应用。”这句话评价的很到位。虽然它很常用但是占代码时间比例不多,容易忘记。本文稍微详细介绍一下线程池的整体知识并举例使用,便于回忆。


1. 了解线程池

如果想让线程池执行任务的话需要实现的 Runnable 接口或 Callable 接口。 Runnable 接口或 Callable 接口实现类都可以被ThreadPoolExecutor 或 ScheduledThreadPoolExecutor 执行。两者区别: Runnable 接口不会返回结果, Callable 接口可以返回结果。

Java里面线程池的顶级接口是 java.util.concurrent.Executor ,但是严格意义上讲 Executor 并不是一个线程池,而只是一个执行线程的工具类。真正的线程池接口是 java.util.concurrent.ExecutorService


2. Executors 线程池

2.1. 常见线程池种类
线程池名称 描述
newSingleThreadExecutor() 创建一个单线程的线程池,此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
newFixedThreadPool(int nThreads) nThreads:线程数。创建固定大小的线程池,每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。
newCachedThreadPool() 创建一个可缓存的线程池,此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
newScheduledThreadPool(int corePoolSize) 创建一个大小无限的线程池,此线程池支持定时以及周期性执行任务的需求。

2.2. 常用线程池方法
方法 解释
Future<?> submit(Runnable task) 获取线程池中的某一个线程对象,并执行。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功。( Future接口:用来记录线程任务执行完毕后产生的结果。)
public void execute(Runnable command) 用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否。
public void shutdown() 注意:Executors类包含一组方法,它们可以将其他一些常见的类闭包对象,例如,java.security.PrivilegedAction转换为Callable表单,以便提交它们。销毁或关闭线程池

2.3. Executors 使用说明

要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在 java.util.concurrent.Executors 线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用 Executors 工程类来创建线程池对象。

但是《阿里巴巴Java开发手册》中却强制线程池不允许使用 Executors,而是使用 ThreadPoolExecutor ,下文详细介绍。


2.4. Executors创建线程池使用示例(工具类)
class MyPool implements Runnable {
    
    
    @Override
    public void run() {
    
    
        System.out.println("线程" + Thread.currentThread().getName() + "的run方法正在运行");
    }
}
public class Demo {
    
    
    public static void main(String[] args) {
    
    
        // 创建一个固定大小的线程池
        ExecutorService es = Executors.newFixedThreadPool(2);
        MyPool pool = new MyPool();
        es.submit(pool);
        es.submit(pool);
        es.submit(pool);
        // 关闭线程池
        es.shutdown();
    }
}

输出
线程pool-1-thread-1的run方法正在运行
线程pool-1-thread-2的run方法正在运行
线程pool-1-thread-1的run方法正在运行

3. ThreadPoolExecutor 线程池

3.1. 了解ThreadPoolExecutor

《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 工具类去创建,因为方法参数没有或只有一个,难以满足场景需求。而是通过 ThreadPoolExecutor 的方式,提供的入参很多,可定制线程池。这样的处理方式可以加明确线程池的运行规则,规避资源耗尽的风险。

Executors.xxxxxx 返回线程池对象的弊端如下:
newFixedThreadPool newSingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM(内存不足)。
newCachedThreadPool newScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM


3.2. ThreadPoolExecutor 构造函数源码
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    
    
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.acc = System.getSecurityManager() == null ?
        null :
    AccessController.getContext();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}
  • 源码参数说明(可结合上述例子理解)
参数 描述
corePoolSize 核心线程数,最小可以同时运行的线程数量。即使线程是空闲的,也要保留在池中的线程数,除非设置了allowCoreThreadTimeOut = true(默认值是false)
maximumPoolSize 池中允许可以同时运行的最大线程数
keepAliveTime 当线程数大于 corePoolSize 时,这是多余的空闲线程在终止前等待新任务的最大时间。超过了就会被回收销毁
unit keepAliveTime参数的时间单位
workQueue 用于在执行任务之前保存任务的队列。该队列将只保存由 execute 方法提交的可运行任务。
threadFactory executor 创建新线程会用到这个工厂类。 默认实现 Executors.DefaultThreadFactory
handler 饱和策略。当由于达到线程边界和队列容量而阻塞执行时使用的处理程序
  • 异常说明(可看上述源码理解)

    1. 如果满足 corePoolSize < 0 || keepAliveTime < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize 这个条件,会抛出 IllegalArgumentException 超出有效范围异常
    2. workQueuethreadFactoryhandler,不能为空,否则抛出 NullPointerException 空指针异常
  • handler 参数使用:饱和策略定义

    如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了的时候,ThreadPoolTaskExecutor 定义一些策略来处理问题:

    参数值 描述
    ThreadPoolExecutor.CallerRunsPolicy 调用执行自己的线程运行任务,也就是直接在调用 execute 方法的线程中运行( run )被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。
    ThreadPoolExecutor.AbortPolicy 抛出 RejectedExecutionException 来拒绝新任务的
    ThreadPoolExecutor.DiscardPolicy 不处理新任务,它会默默地丢弃掉。
    ThreadPoolExecutor.DiscardOldestPolicy 除非执行程序关闭,不然将丢弃最早的未还处理的任务请求,然后重试执行。

    工作原理示例说明:Spring 通过 ThreadPoolTaskExecutor 或者我们直接通过 ThreadPoolExecutor 的构造函数创建线程池的时候,当我们不指定RejectedExecutionHandler 饱和策略的话来配置线程池的时候默认使用的是 ThreadPoolExecutor.AbortPolicy 。在默认情况下,ThreadPoolExecutor 将抛出 RejectedExecutionException 来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。 对于可伸缩的应用程序,建议使用ThreadPoolExecutor.CallerRunsPolicy 。当最大池被填满时,此策略为我们提供可伸缩队列。


3.3. 线程池工作原理流程(重要)

首先我们再次理解一下:
corePoolSize(核心线程数):能够同时执行任务请求的最大值;
maximumPoolSize(最大线程数):一般要大于 corePoolSize,包含核心线程数,初始大小就 corePoolSize,是为了留有一定的容灾空间。就好比公交车上有16个座位,但是最大乘客数可以更多,因为多出来的可以站着。
workQueue(队列容量):超过 corePoolSize 的任务部分,独立计算,不被包含在maximumPoolSize中,可以理解为在公交车前排队的乘客。

  1. 线程池线程执行数 <= corePoolSize(核心线程数):因为线程池必须保持至少有 corePoolSize 个线程,他一开始就会创建corePoolSize 个线程,即使有剩余的空闲线程不干事儿。除非设置了allowCoreThreadTimeOut = true(默认值是false),核心线程将使用 keepAliveTime 超时等待工作。
  2. corePoolSize(核心线程数) < 线程池线程执行数 <= corePoolSize(核心线程数)+ workQueue(队列容量):超出 corePoolSize 的部分会被放到你指定的队列中,等待核心线程执行完毕再进行补充。
  3. 线程池线程执行数 > workQueue(队列容量)+ corePoolSize(核心线程数):多出的新任务请求会放到线程池中直到达到了 maximumPoolSize(最大线程数),这时候的任务将会通过创建新的线程去执行,减缓线程池压力。
  4. 当 线程池压力减下去的时候,最大线程池中那些新建的线程就会处于空闲状态。这时同样如果你配置了allowCoreThreadTimeOut = true(默认值是false),当空闲状态(不干活儿)超过了你设置的 keepAliveTime(最大等待执行任务时间),就会销毁掉这些空闲线程以释放内存 。
  5. 线程池线程执行数 > workQueue(队列容量)+ maximumPoolSize最大线程数():将由你设置的饱和策略处理新任务(伸缩队列、替换或拒绝)

3.4. ThreadPoolExecutor 线程池使用示例
class MyPool implements Runnable {
    
    
    @Override
    public void run() {
    
    
        System.out.println("线程" + Thread.currentThread().getName() + "的run方法Start");
        working();
        System.out.println("线程" + Thread.currentThread().getName() + "的run方法End");
    }

    // 模拟线程执行耗时5s
    private void working() {
    
    
        try {
    
    
            Thread.sleep(5000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }
}

/**
 * @Author 白忆宇
 */
public class Demo02 {
    
    
    private static final int CORE_POOL_SIZE = 5; // 核心线程数
    private static final int MAX_POOL_SIZE = 10; // 最大线程数
    private static final Long KEEP_ALIVE_TIME = 1L; // 等待新任务的最大时间(1s)
    private static final int QUEUE_CAPACITY = 50; // 任务队列为ArrayBlockingQueue,容量50;

    public static void main(String[] args) {
    
    
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                CORE_POOL_SIZE,
                MAX_POOL_SIZE,
                KEEP_ALIVE_TIME,
                TimeUnit.SECONDS, // 等待时间单位
                new ArrayBlockingQueue<>(QUEUE_CAPACITY), // 使用初始固定容量的队列
                Executors.defaultThreadFactory(), // 线程工厂类(默认值)
        new ThreadPoolExecutor.CallerRunsPolicy()); // 饱和策略
        // 开启10个线程
        for (int i = 0; i < 10; i++) {
    
    
            //创建MyPool对象
            MyPool worker = new MyPool();
            // 执行线程
            executor.execute(worker);
        }
        //终止线程池
        executor.shutdown();
        // 保持主线程不结束,等其它协程结束
        // isTerminated()方法说明:
        // 如果该执行器在shutdown或shutdownNow后正在终止,但尚未完全终止,返回true。一般用于调试
        while (!executor.isTerminated()) {
    
    
        }
        System.out.println("多线程执行完毕");
    }
}
// 输出
线程pool-1-thread-1的run方法Start
线程pool-1-thread-3的run方法Start
线程pool-1-thread-4的run方法Start
线程pool-1-thread-2的run方法Start
线程pool-1-thread-5的run方法Start
线程pool-1-thread-4的run方法End
线程pool-1-thread-5的run方法End
线程pool-1-thread-2的run方法End
线程pool-1-thread-2的run方法Start
线程pool-1-thread-1的run方法End
线程pool-1-thread-3的run方法End
线程pool-1-thread-1的run方法Start
线程pool-1-thread-5的run方法Start
线程pool-1-thread-4的run方法Start
线程pool-1-thread-3的run方法Start
线程pool-1-thread-4的run方法End
线程pool-1-thread-1的run方法End
线程pool-1-thread-2的run方法End
线程pool-1-thread-5的run方法End
线程pool-1-thread-3的run方法End
多线程执行完毕

输出过程解释: 因为设置了核心线程数为5,会先同时打印5行,5秒执行后再打印同时10行(结束的五个线程和新开始的五个线程),最后5秒执行后再同时打印5行,结束后5个线程。总共20行,10个线程的开始和结束。

4. 使用线程池的好处

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

5. 随笔

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/m0_48489737/article/details/127698480
今日推荐