JUC基础09——线程池

333 阅读14分钟

线程池

为什么要使用线程池

在不使用线程池的情况下,我们使用多线程执行任务会存在一些弊端:

  1. 难以管理线程生命周期:线程的创建、启动、暂停终止等操作,容易出现并发问题,代码复杂度也会增加,导致手动管理线程生命周期很困难
  2. 难以控制并发数:手动控制并发执行任务的数量,在执行的任务过多时,容易造成系统资源不足或者性能下降,任务太少,又无法充分利用资源
  3. 难以控制系统资源消耗:每次创建线程都需要分配管理系统资源,包括内存、线程栈等。在频繁的创建和销毁线程,会增加系统的资源消耗,降低系统的性能,并可能导致资源耗尽的风险

当我们使用线程池就可以有效地解决这些问题,通过维护合理数量的线程来处理并发任务,可以避免频繁创建和销毁线程,减少上下文切换和任务调度开销,而且线程池提供了对线程的管理和监控机制,能根据任务的到达情况动态分配和管理这些线程,从而有效地管理线程的生命周期,提高系统资源的利用率。

线程池的概念

线程池是一种使用池化技术管理和复用线程的并发编程机制,它将多个线程预先存储在一个 “ 池子 ” 内,这些线程可以被重复使用来执行多个任务。从而避免频繁创建和销毁线程所带来的开销。 线程池的核心组成部分:

  1. 任务队列(taskQueue):用于等待执行的任务,线程池中的线程空闲时,会从队列中取出任务进行执行
  2. 任务接口(task):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等
  3. 线程池管理器(threadPool):负责创建和管理线程池,包括负责创建、启动、暂停和销毁线程,在线程池初始化时会创建一定数量的线程,并在需要时动态地调整线程数量
  4. 工作线程(poolWorker):线程池中的实际执行者,在没有任务时处于等待状态,可以循环地执行任务

线程池架构说明

java中线程池是通过Executor框架实现的,该框架中用到了Executor,Executors(代表工具类),ExecutorService,ThreadPoolExecutor这几个类

image.png

线程池的使用

创建线程池

使用 Executors 工具类创建线程池

  • Executors.newFixedThreadPool(int i) :创建一个拥有 i 个线程的线程池

    • 执行长期的任务,性能好很多
    • 创建一个定长线程池,可控制线程数最大并发数,超出的线程会在队列中等待
  • Executors.newSingleThreadExecutor:创建一个只有1个线程的 单线程池

    • 一个任务一个任务执行的场景
    • 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行
  • Executors.newCacheThreadPool(); 创建一个可扩容的线程池

    • 执行很多短期异步的小程序或者负载教轻的服务器
    • 创建一个可缓存线程池,如果线程长度超过处理需要,可灵活回收空闲线程,如无可回收,则新建新线程
  • Executors.newScheduledThreadPool(int corePoolSize):线程池支持定时以及周期性执行任务,创建一个corePoolSize为传入参数,最大线程数为整形的最大数的线程池

使用 ThreadPoolExecutor 类创建线程池

ThreadPoolExecutor executor = new ThreadPoolExecutor(  
    5, // 核心线程数  
    10, // 最大线程数  
    60L, // 线程空闲超过60秒则销毁  
    TimeUnit.SECONDS,  
    new LinkedBlockingQueue<Runnable>() // 任务队列  
);

使用案例:模拟20个用户来办理业务,用5个线程处理20个任务请求

示例代码如下:

public static void main(String[] args) {
    ExecutorService executorService = Executors.newFixedThreadPool(5);
    //也可以使用 ThreadPoolExecutor 创建线程池
    
//ThreadPoolExecutor executorService = new ThreadPoolExecutor(
//        5, // 核心线程数
//        10, // 最大线程数
//        60L, // 线程空闲超过60秒则销毁
//        TimeUnit.SECONDS,
//        new LinkedBlockingQueue<Runnable>() // 任务队列
//);

    try{
        for(int i=1;i<=20;i++){
            final  int tmpInt =i;
            executorService.execute(()->{
                System.out.println(Thread.currentThread().getName()+"\t 线程给"+String.valueOf(tmpInt)+"号办理了业务");
            });
        }
    }finally {
        executorService.shutdown();
    }
}

执行结果:共有5个线程,办理了20个客户的业务

pool-1-thread-1	 线程给1号客户办理了业务
pool-1-thread-4	 线程给4号客户办理了业务
pool-1-thread-3	 线程给3号客户办理了业务
pool-1-thread-2	 线程给2号客户办理了业务
pool-1-thread-3	 线程给8号客户办理了业务
pool-1-thread-4	 线程给7号客户办理了业务
pool-1-thread-5	 线程给5号客户办理了业务
pool-1-thread-1	 线程给6号客户办理了业务
pool-1-thread-5	 线程给12号客户办理了业务
pool-1-thread-4	 线程给11号客户办理了业务
pool-1-thread-3	 线程给10号客户办理了业务
pool-1-thread-2	 线程给9号客户办理了业务
pool-1-thread-3	 线程给16号客户办理了业务
pool-1-thread-4	 线程给15号客户办理了业务
pool-1-thread-5	 线程给14号客户办理了业务
pool-1-thread-1	 线程给13号客户办理了业务
pool-1-thread-5	 线程给20号客户办理了业务
pool-1-thread-4	 线程给19号客户办理了业务
pool-1-thread-3	 线程给18号客户办理了业务
pool-1-thread-2	 线程给17号客户办理了业务

创建周期性执行任务的线程池

线程池支持定时以及周期性执行任务,创建一个corePoolSize为传入参数,最大线程数为Integer.MAX_VALUE 的线程池

    Executors.newScheduledThreadPool(int corePoolSize)

其底层使用的是ScheduledThreadPoolExecutor实现,ScheduledThreadPoolExecutor是ThreadPoolExecutor的子类

public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue());
}

执行方法

    /**
     * @throws RejectedExecutionException {@inheritDoc}
     * @throws NullPointerException       {@inheritDoc}
     * command:执行的任务 Callable或Runnable接口实现类
	 * delay:延时执行任务的时间
	 * unit:延迟时间单位
     */
    public ScheduledFuture<?> schedule(Runnable command,
                                       long delay,
                                       TimeUnit unit)



    /**
     * @throws RejectedExecutionException {@inheritDoc}
     * @throws NullPointerException       {@inheritDoc}
     * @throws IllegalArgumentException   {@inheritDoc}
     * command:执行的任务 Callable或Runnable接口实现类
	 * initialDelay 第一次执行任务延迟时间
	 * period 连续执行任务之间的周期,从上一个任务开始执行时计算延迟多少开始执行下一个任务,但是还会等上一个任务结束之后。
	 * unit:延迟时间单位
     */
    public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                                  long initialDelay,
                                                  long period,
                                                  TimeUnit unit)
                                                  
                                                  

    /**
     * @throws RejectedExecutionException {@inheritDoc}
     * @throws NullPointerException       {@inheritDoc}
     * @throws IllegalArgumentException   {@inheritDoc}
     * command:执行的任务 Callable或Runnable接口实现类
	 * initialDelay 第一次执行任务延迟时间
	 * delay:连续执行任务之间的周期,从上一个任务全部执行完成时计算延迟多少开始执行下一个任务
	 * unit:延迟时间单位
     */
    public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                                     long initialDelay,
                                                     long delay,
                                                     TimeUnit unit)

线程池 7 大参数

  • corePoolSize:核心线程数(线程池中常驻的线程)

    • 在创建线程池后,当有请求任务来时,就会安排池中的线程去执行请求任务,近似理解为今日当值线程
    • 当线程池中的线程数目达到corePoolSize后,就会把到达的队列放到缓存队列中
  • maximumPoolSize:能容纳的最大线程数,此值必须 大于等于 1

    • 当提交任务数大于 corePoolSize 的时候,会优先将任务放到 workQueue 阻塞队列中。当阻塞队列饱和后,会扩充线程池中线程数,直到达到maximumPoolSize 最大线程数配置。此时,再多余的任务,则会触发线程池 的拒绝策略了
  • keepAliveTime:空闲线程存活时间

    • 当线程池的数量超过 corePoolSize ,且空闲时间达到了 keepAliveTime,此时,空闲线程就会被销毁,只留下 核心线程 运行
  • unit:keepAliveTime的单位(空闲线程存活时间单位)

    • TimeUnit.NANOSECONDS;纳秒
    • TimeUnit.MICROSECONDS;微秒
    • TimeUnit.MILLISECONDS;毫秒
    • TimeUnit.SECONDS;秒
    • TimeUnit.MINUTES;分钟
    • TimeUnit. HOURS;小时
    • TimeUnit.DAYS;天
  • workQueue:任务队列,存放被提交还未执行的任务

  • ThreadFactory:创建线程的工厂类

  • handler:拒绝策略,当任务队列满了之后,且工作线程数大于最大线程数时,就会触发线程池的拒绝策略

拒绝策略

  • CallerRunsPolicy:当线程池已经达到最大线程数,且任务队列已经满了,新提交的任务将会被提交者所在的线程执行。这种策略可以确保新提交的任务一定会被执行,但是如果提交任务的线程也处于高负载的状态时,可能会导致性能下降
  • AbortPolicy:线程池默认的拒绝策略,当线程池已经达到最大线程数,并且任务队列已经满了,新提交的任务将被立即拒绝并抛出RejectedExecutionException异常
  • DiscardPolicy:当线程池已经达到最大线程数,并且工作队列已经满了,新提交的任务将会被直接丢弃,且不会抛出任何异常
  • DiscardOldestPolicy:当线程池达到最大线程数,且任务队列已经满了,新提交的任务将会替换带队列中最早的任务。这种策略可以避免新提交的任务直接被丢弃,但是替换掉最早的任务可能导致某些任务无法执行

除了上述4种拒绝策略外,还可以通过实现 RejectedExecutionHandler 接口,重写 rejectedExecution() 方法 自定义拒绝策略

线程池的运行流程

2.png

流程描述:

  1. 在创建了线程池后,等待提交过来的任务请求

  2. 当调用execute()方法添加一个请求任务时,线程池会做出如下判断

    • 如果正在运行的线程数量小于corePoolSize,则立即创建线程运行这个任务
    • 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列
    • 如果这时候队列满了,并且正在运行的线程数量还小于maximumPoolSize,那么还是创建非核心线程like运行这个任务;
    • 如果队列满了并且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行
  3. 当一个线程完成任务时,它会从队列中取下一个任务来执行

  4. 当一个线程无事可做,空闲超过一定时间(keepAliveTime)时,线程池会判断:

    • 如果当前运行的线程数量大于 corePoolSize ,那么这个线程就会被停掉
    • 线程池的所有任务完成后,最终都会缩到 corePoolSize 的大小

自定义线程池

创建一个核心线程为2,最大线程数为5,队列容量为3 的线程池。

使用默认拒绝策略(new ThreadPoolExecutor.AbortPolicy())

public static void main(String[] args) {
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
            2,//核心线程数
            5,//最大线程数
            1,//空闲线程存活时间
            TimeUnit.SECONDS,//存活时间单位
            new ArrayBlockingQueue<>(3),//任务队列,指定容量为3
            Executors.defaultThreadFactory(),//默认 ThreadFactory
            new ThreadPoolExecutor.AbortPolicy()//默认拒绝策略
    );
    try{
        for(int i=1;i<=9;i++){
            final  int tmpInt =i;
            threadPoolExecutor.execute(()->{
                System.out.println(Thread.currentThread().getName()+"\t 线程给"+String.valueOf(tmpInt)+"号客户办理了业务");
            });
        }
    }finally {
        threadPoolExecutor.shutdown();
    }
}

执行结果:在执行到第9个任务的时候,触发了拒绝策略

pool-1-thread-1	 线程给1号客户办理了业务
pool-1-thread-2	 线程给2号客户办理了业务
pool-1-thread-4	 线程给7号客户办理了业务
pool-1-thread-3	 线程给6号客户办理了业务
pool-1-thread-1	 线程给4号客户办理了业务
pool-1-thread-2	 线程给3号客户办理了业务
pool-1-thread-4	 线程给5号客户办理了业务
pool-1-thread-5	 线程给8号客户办理了业务
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task com.avgrado.demo.thread.ThreadPoolDemo$$Lambda$1/932172204@76fb509a rejected from java.util.concurrent.ThreadPoolExecutor@300ffa5d[Running, pool size = 5, active threads = 1, queued tasks = 0, completed tasks = 7]
	at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2047)
	at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:823)
	at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1369)
	at com.avgrado.demo.thread.ThreadPoolDemo.main(ThreadPoolDemo.java:33)

我们设置的拒绝策略是默认的AbortPolicy,触发时会抛异常

触发条件是,请求的线程大于 阻塞队列大小 + 最大线程数 = 8 的时候,也就是说第9个线程来获取线程池中的线程时,就会触发拒绝策略,抛出异常导致程序退出

使用回退拒绝策略(new ThreadPoolExecutor.CallerRunsPolicy())

CallerRunsPolicy拒绝策略,也称为回退策略,触发时会把新提交的任务给提交者所在的线程执行

public static void main(String[] args) {
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
            2,//核心线程数
            5,//最大线程数
            1,//空闲线程存活时间
            TimeUnit.SECONDS,//存活时间单位
            new ArrayBlockingQueue<>(3),//任务队列,指定容量为3
            Executors.defaultThreadFactory(),//默认 ThreadFactory
            new ThreadPoolExecutor.CallerRunsPolicy()//CallerRunsPolicy拒绝策略
    );
    try{
        for(int i=1;i<=9;i++){
            final  int tmpInt =i;
            threadPoolExecutor.execute(()->{
                System.out.println(Thread.currentThread().getName()+"\t 线程给"+String.valueOf(tmpInt)+"号客户办理了业务");
            });
        }
    }finally {
        threadPoolExecutor.shutdown();
    }
}

执行结果:最终是main线程执行了第9个任务

pool-1-thread-1	 线程给1号客户办理了业务
pool-1-thread-3	 线程给6号客户办理了业务
pool-1-thread-1	 线程给3号客户办理了业务
pool-1-thread-4	 线程给7号客户办理了业务
main	 线程给9号客户办理了业务
pool-1-thread-2	 线程给2号客户办理了业务
pool-1-thread-5	 线程给8号客户办理了业务
pool-1-thread-1	 线程给5号客户办理了业务
pool-1-thread-3	 线程给4号客户办理了业务

使用Discard 拒绝策略(new ThreadPoolExecutor.Discard())

Discard 拒绝策略触发时,新提交的任务将会被直接丢弃,且不会抛出任何异常

public static void main(String[] args) {
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
            2,//核心线程数
            5,//最大线程数
            1,//空闲线程存活时间
            TimeUnit.SECONDS,//存活时间单位
            new ArrayBlockingQueue<>(3),//任务队列,指定容量为3
            Executors.defaultThreadFactory(),//默认 ThreadFactory
            new ThreadPoolExecutor.Discard()//Discard 拒绝策略
    );
    try{
        for(int i=1;i<=9;i++){
            final  int tmpInt =i;
            threadPoolExecutor.execute(()->{
                System.out.println(Thread.currentThread().getName()+"\t 线程给"+String.valueOf(tmpInt)+"号客户办理了业务");
            });
        }
    }finally {
        threadPoolExecutor.shutdown();
    }
}

执行结果:并不抛出异常

pool-1-thread-1	 线程给1号客户办理了业务
pool-1-thread-3	 线程给6号客户办理了业务
pool-1-thread-1	 线程给3号客户办理了业务
pool-1-thread-2	 线程给2号客户办理了业务
pool-1-thread-1	 线程给5号客户办理了业务
pool-1-thread-3	 线程给4号客户办理了业务
pool-1-thread-5	 线程给8号客户办理了业务
pool-1-thread-4	 线程给7号客户办理了业务

使用 DiscardOldestPolicy拒绝策略(new ThreadPoolExecutor.DiscardOldestPolicy())

DiscardOldestPolicy 拒绝策略触发时,新提交的任务将会替换带队列中最早的任务,且不会抛出任何异常

public static void main(String[] args) {
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
            2,//核心线程数
            5,//最大线程数
            1,//空闲线程存活时间
            TimeUnit.SECONDS,//存活时间单位
            new ArrayBlockingQueue<>(3),//任务队列,指定容量为3
            Executors.defaultThreadFactory(),//默认 ThreadFactory
            new ThreadPoolExecutor.DiscardOldestPolicy()//DiscardOldestPolicy拒绝策略
    );
    try{
        for(int i=1;i<=9;i++){
            final  int tmpInt =i;
            threadPoolExecutor.execute(()->{
                System.out.println(Thread.currentThread().getName()+"\t 线程给"+String.valueOf(tmpInt)+"号客户办理了业务");
            });
        }
    }finally {
        threadPoolExecutor.shutdown();
    }
}

执行结果:可以看出3号客户的任务被直接丢弃掉了

pool-1-thread-1	 线程给1号客户办理了业务
pool-1-thread-3	 线程给6号客户办理了业务
pool-1-thread-2	 线程给2号客户办理了业务
pool-1-thread-5	 线程给8号客户办理了业务
pool-1-thread-3	 线程给5号客户办理了业务
pool-1-thread-1	 线程给4号客户办理了业务
pool-1-thread-4	 线程给7号客户办理了业务
pool-1-thread-2	 线程给9号客户办理了业务

线程池的参数配置

如何在生产环境中配置 corePoolSizemaximumPoolSize

根据具体的业务来配置,分为

  • CPU 密集型:
    • CPU密集的意思是该任务需要大量运算而没有阻塞,CPU一直全速运行
    • CPU 密集任务只有在真正的多核CPU上才可能得到加速(通过多线程),在单核CPU上,无论开几个模拟的多线程,该任务都不可能得到加锁,因为CPU的总算力是固定的
    • CPU 密集型任务配置尽可能少的配置线程数量:线程数量 = CPU核数+1个线程数
  • I O 密集型
    • I O 密集型不是在一直执行任务,应该多配置线程数量
    • IO密集型,即该任务需要大量的IO操作,即大量的阻塞
    • 在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力花费在等待上
    • 所以IO密集型任务中使用多线程可以大大的加速程序的运行,即使在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。
    • IO密集时,大部分线程都被阻塞,故需要多配置线程数:线程数量=CPU核数 / (1 - 阻塞系数) 阻塞系数为0.8~0.9,比如8核CPU ,阻塞系数取0.9 ,设置 线程数量 = 8/(1-0.9) = 80