线程池参数详解

7个参数的用途

创建线程池一共有7个参数,从源码可知,corePoolSize和maximumPoolSize都不能小于0,且核心线程数不能大于最大线程数。

corePoolSize

线程池核心线程数量,核心线程不会被回收,即使没有任务执行,也会保持空闲状态。

maximumPoolSize

池允许最大的线程数,当线程数量达到corePoolSize,且workQueue队列塞满任务了之后,继续创建线程。

keepAliveTime

超过corePoolSize之后的“临时线程”的存活时间。

unit

keepAliveTime的单位。

workQueue

当前线程数超过corePoolSize时,新的任务会处在等待状态,并存在workQueue中,BlockingQueue是一个先进先出的阻塞式队列实现,底层实现会涉及Java并发的AQS机制

threadFactory

创建线程的工厂类,通常我们会自定一个threadFactory设置线程的名称,这样就可以知道线程是由哪个工厂类创建的,可以快速定位。

handler

线程池执行拒绝策略,当线数量达到maximumPoolSize大小,并且workQueue也已经塞满了任务的情况下,线程池会调用handler拒绝策略来处理请求。

系统默认的拒绝策略有以下几种:

  1. AbortPolicy:为线程池默认的拒绝策略,该策略直接抛异常处理。

  2. DiscardPolicy:直接抛弃不处理。

  3. DiscardOldestPolicy:丢弃队列中最老的任务。

  4. CallerRunsPolicy:将任务分配给当前执行execute方法线程来处理。

我们还可以自定义拒绝策略,只需要实现RejectedExecutionHandler接口即可,友好的拒绝策略实现有如下:

  1. 将数据保存到数据,待系统空闲时再进行处理

  2. 将数据用日志进行记录,后由人工处理

ThreadPoolExecutor创建线程方式

通过下面的demo来了解ThreadPoolExecutor创建线程的过程。

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
 
/**
 * 测试ThreadPoolExecutor对线程的执行顺序
 **/
public class ThreadPoolSerialTest {
    public static void main(String[] args) {
        //核心线程数
        int corePoolSize = 3;
        //最大线程数
        int maximumPoolSize = 6;
        //超过 corePoolSize 线程数量的线程最大空闲时间
        long keepAliveTime = 2;
        //以秒为时间单位
        TimeUnit unit = TimeUnit.SECONDS;
        //创建工作队列,用于存放提交的等待执行任务
        BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<Runnable>(2);
        ThreadPoolExecutor threadPoolExecutor = null;
        try {
            //创建线程池
            threadPoolExecutor = new ThreadPoolExecutor(corePoolSize,
                    maximumPoolSize,
                    keepAliveTime,
                    unit,
                    workQueue,
                    new ThreadPoolExecutor.AbortPolicy());
 
            //循环提交任务
            for (int i = 0; i < 8; i++) {
                //提交任务的索引
                final int index = (i + 1);
                threadPoolExecutor.submit(() -> {
                    //线程打印输出
                    System.out.println("大家好,我是线程:" + index);
                    try {
                        //模拟线程执行时间,10s
                        Thread.sleep(10000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                });
                //每个任务提交后休眠500ms再提交下一个任务,用于保证提交顺序
                Thread.sleep(500);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            threadPoolExecutor.shutdown();
        }
    }
}

执行结果:

 

这里描述一下执行的流程:

  • 首先通过 ThreadPoolExecutor 构造函数创建线程池;

  • 执行 for 循环,提交 8 个任务(恰好等于maximumPoolSize[最大线程数] + capacity[队列大小]);

  • 通过 threadPoolExecutor.submit 提交 Runnable 接口实现的执行任务;

  • 提交第1个任务时,由于当前线程池中正在执行的任务为 0 ,小于 3(corePoolSize 指定),所以会创建一个线程用来执行提交的任务1;

  • 提交第 2, 3 个任务的时候,由于当前线程池中正在执行的任务数量小于等于 3 (corePoolSize 指定),所以会为每一个提交的任务创建一个线程来执行任务;

  • 当提交第4个任务的时候,由于当前正在执行的任务数量为 3 (因为每个线程任务执行时间为10s,所以提交第4个任务的时候,前面3个线程都还在执行中),此时会将第4个任务存放到 workQueue 队列中等待执行;

  • 由于 workQueue 队列的大小为 2 ,所以该队列中也就只能保存 2 个等待执行的任务,所以第5个任务也会保存到任务队列中;

  • 当提交第6个任务的时候,因为当前线程池正在执行的任务数量为3,workQueue 队列中存储的任务数量也满了,这时会判断当前线程池中正在执行的任务的数量是否小于6(maximumPoolSize指定);

  • 如果小于 6 ,那么就会新创建一个线程来执行提交的任务 6;

  • 执行第7,8个任务的时候,也要判断当前线程池中正在执行的任务数是否小于6(maximumPoolSize指定),如果小于6,那么也会立即新建线程来执行这些提交的任务;

  • 此时,6个任务都已经提交完毕,那 workQueue 队列中的等待 任务4 和 任务5 什么时候执行呢?

  • 当任务1执行完毕后(10s后),执行任务1的线程并没有被销毁掉,而是获取 workQueue 中的任务4来执行;

  • 当任务2执行完毕后,执行任务2的线程也没有被销毁,而是获取 workQueue 中的任务5来执行;

通过上面流程的分析,也就知道了之前案例的输出结果的原因。其实,线程池中会线程执行完毕后,并不会被立刻销毁,线程池中会保留 corePoolSize 数量的线程,当 workQueue 队列中存在任务或者有新提交任务时,那么会通过线程池中已有的线程来执行任务,避免了频繁的线程创建与销毁,而大于 corePoolSize 小于等于 maximumPoolSize 创建的线程,则会在空闲指定时间(keepAliveTime)后进行回收。

ThreadPoolExecutor拒绝策略

在上面的测试中,我设置的执行线程总数恰好等于maximumPoolSize[最大线程数] + capacity[队列大小],因此没有出现需要执行拒绝策略的情况,因此在这里,我再增加一个线程,提交9个任务,来演示不同的拒绝策略。

1、AbortPolicy

09f46d29aaee89592e9113a84d3c1c9e.png

2、CallerRunsPolicy

*bf4b8754c4616c781da520a17d10071d.png*

3、DiscardPolicy

47c534643838b8757202ac4ffb6813e3.png

4、DiscardOldestPolicy

dea00214a0b24a3fc71e1280b71e1ebe.png 

Demo2

public class ThreadPool {

    public static void main(String[] args) {
        // 创建线程池 , 参数含义 :(核心线程数,最大线程数,加开线程的存活时间,时间单位,任务队列长度)
        ThreadPoolExecutor pool = new ThreadPoolExecutor(5, 8,
                0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<Runnable>(2));

        //设置a的值范围在:a = (corePoolSize-1) ~ (max+queue+1) ,分析:任务数 与 活跃线程数,核心线程数,队列长度,最大线程数的关系。
        int a = 7;

        for (int i = 1; i <= a; i++) {
            int j = i;
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    //获取线程名称
                    Thread thread = Thread.currentThread();
                    String name = thread.getName();
                    //输出
                    int activeCount = pool.getActiveCount();
                    System.out.println("任务:"+j+"-----,线程名称:"+name+"-----活跃线程数:"+activeCount);
                }
            });
        }
        //关闭线程池
        pool.shutdown();
    }
}

输出结果,观察关系:

任务数 a = 4 , 活跃线程数4 , 任务数 < 核心线程数。

任务数 a = 5 , 活跃线程数5 , 任务数 = 核心线程数。

任务数 a = 6 , 活跃线程数5 , 任务数 < 核心线程数5 + 队列长度2 。

任务数 a = 7 , 活跃线程数5 , 任务数 = 核心线程数5 + 队列长度2 。

任务数 a = 8 , 活跃线程数6 , 任务数 < 最大线程数8 + 队列长度2 . 活跃线程数是在核心线程数5的基础上.加1个活跃线程。

任务数 a = 9 , 活跃线程数7 , 任务数 < 最大线程数8 + 队列长度2.  活跃线程数是在核心线程数5的基础上.加2个活跃线程。

任务数 a = 10 , 活跃线程数8 , 任务数 = 最大线程数8 + 队列长度2.  活跃线程数是在核心线程数5的基础上.加3个活跃线程。

任务数 a = 11 , 活跃线程数8 , 任务数 > 最大线程数8 + 队列长度2 。抛出异常RejectedExecutionException

总结:

1、随着任务数量的增加,会增加活跃的线程数。

2、当活跃的线程数 =   核心线程数,此时不再增加活跃线程数,而是往任务队列里堆积。

3、当任务队列堆满了,随着任务数量的增加,会在核心线程数的基础上加开线程。

4、直到活跃线程数 = 最大线程数,就不能增加线程了。

5、如果此时任务还在增加,则: 任务数11 > 最大线程数8 + 队列长度2 ,默认的拒绝策略会抛出异常RejectedExecutionException,拒绝任务。

 

posted @ 2021-06-07 15:59  江南大才子  阅读(3073)  评论(0编辑  收藏  举报