Java并发编程:线程池

什么是线程池

管理一系列线程的资源池。当有任务要处理时,直接从线程池中获取线程来处理,处理完之后线程不会立即被销毁,而是等待下一个任务。

为什么要使用线程池

池化技术:线程池、数据库连接池、HTTP连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。

线程池提供了一种限制和管理资源的方式。每个线程池还维护了一些基本统计信息,比如已完成任务的数量。

使用线程池的好处:

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

如果不使用线程池,有可能会造成系统创建大量同类线程而导致消耗完内存或者"过度切换"的问题。

创建线程池的2种方式

通过ThreadPoolExecutor构造函数来创建(推荐)

主要是实现了顶层接口 Executor,在该接口中存在唯一一个抽象方法execute,主要用于提交任务。

实战举例

java 复制代码
public class PrintStoryThreadPoolExecutorMain {
    public static final String text = "今天又是阳光明媚的一天";

    public static void main(String[] args) {
        ExecutorService service = new ThreadPoolExecutor(10, 10, 60L, TimeUnit.SECONDS,
                        new ArrayBlockingQueue<>(10));

        for (int i = 1; i <= 2; i++){
            System.out.println("向线程池提交Runnable任务,开始运行");
            service.submit(new PrintStoryThreadPool(text, 200 * i));
        }
        service.shutdown();
    }
}

通过Executor框架的工具类Executors来创建(不推荐)。

可以创建多种类型的ThreadPoolExecutor,如下:

  • FixedThreadPool:该方法返回一个固定线程数量的线程池,该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,会处理在任务队列中的任务。
  • SingleThreadExecutor:该方法返回一个只有1个线程的线程池。若多余一个任务被提交到线程池,任务会被保存在一个任务队列中,待线程空闲,按照先入先出的顺序执行队列中的任务。
  • CachedThreadPool :该方法返回一个根据实际情况自动调整线程数量的线程池。初始大小为0.当有新任务提交时,如果当前线程池中没有线程可用,则会创建一个新的线程来处理该任务。如果在一段时间内(默认为60s)没有新任务提交,核心线程会超时并被销毁,从而缩减线程池的大小。
  • ScheduledThreadPool:该方法返回一个用来在给定的延迟后运行任务或者定期执行任务的线程池。

实战举例

java 复制代码
public class PrintStoryExecutorsServiceAppMain {
    public static final String text = "今天又是阳光明媚的一天,某小胖7点钟起床洗漱完成,奔向地铁站去往浦东图书馆,在龙阳路换乘时,发现今天有好多人啊," +
            "还都是背着小书包的,某小胖心里想:从地铁站就开始卷了嘛,等下一下车要奔跑哇。";

    public static void main(String[] args) {
        //线程池肯定要有多个线程,提交等多个任务的时候才能用多线程
        ExecutorService service = Executors.newFixedThreadPool(2);
        for (int i = 1; i <= 2; i++){
            System.out.println("向线程池提交Runnable任务,开始运行");
            service.submit(new PrintStoryThreadPool(text, 200 * i));
        }
        service.shutdown();
    }

}

ScheduledThreadPool业务代码里有用到,具体待分析。。。

通过Executors创建线程的缺点

  • FixedThreadPool 和 SingleThreadExecutor :使用的是无界的 LinkedBlockingQueue,任务队列最大长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM。

  • CachedThreadPool :使用的是同步队列 SynchronousQueue, 允许创建的线程数量为 Integer.MAX_VALUE ,如果任务数量过多且执行速度较慢,可能会创建大量的线程,从而导致 OOM。

  • ScheduledThreadPoolSingleThreadScheduledExecutor : 使用的无界的延迟阻塞队列DelayedWorkQueue,任务队列最大长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM。

java 复制代码
// 无界队列 LinkedBlockingQueue
public static ExecutorService newFixedThreadPool(int nThreads) {

    return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());

}

// 无界队列 LinkedBlockingQueue
public static ExecutorService newSingleThreadExecutor() {

    return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()));

}

// 同步队列 SynchronousQueue,没有容量,最大线程数是 Integer.MAX_VALUE`
public static ExecutorService newCachedThreadPool() {

    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());

}

// DelayedWorkQueue(延迟阻塞队列)
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue());
}

线程池常见参数有哪些

ThreadPoolExecutor构造函数源码如下

java 复制代码
// Creates a new {@code ThreadPoolExecutor} with the given initial parameters.
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;
}

3个最重要的参数:

  • corePoolSize :任务队列未达到队列容量时,最大可以同时运行的线程数量。
  • maximumPoolSize :任务队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
  • workQueue:新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。

其他常见参数:

  • keepAliveTime :线程池中的线程数量大于corePoolSize的时候,如果这时没有新的任务提交,多余的空闲线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime才会被回收销毁。线程池回收线程时,会对核心线程和非核心线程一视同仁 ,直到线程池中线程的数量等于corePoolSize,回收过程才会停止。
  • unitkeepAliveTime参数的时间单位
  • threadFactoryexecutor创建新线程的时候会用到
  • handler:饱和策略。

线程池的饱和策略

如果当前同时运行的线程数量达到最大线程数,且队列也已经放满了任务时,ThreadPoolTaskExecutor定义一些策略。

  • ThreadPoolExecutor.AbortPolicy:抛出RejectedExecutionException来拒绝新任务的处理。
  • ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务,也就是直接在调用execute方法的线程中运行被拒绝的任务。如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务的提交速度,影响程序的整体性能。如果应用程序可以接受此延迟,并且要求任何一个任务请求都要被执行的话,可以使用这个策略
  • ThreadPoolExecutor.DiscardPolicy:不处理新任务,直接丢弃。
  • ThreadPoolExecutor.DiscardOldestPolicy:丢弃最早的未处理的任务请求。

举例

Spring通过ThreadPoolTaskExecutor或者我们直接通过ThreadPoolExecutor的构造函数创建线程池的时候,如果不指定RejectedExecutorHandler饱和策略,默认使用的是AbortPolicy。如果队列满了,ThreadPoolExecutor将抛出RejectedExecutionExcception异常来拒绝新的任务,程序会丢失对这个任务的处理。

如果不想丢弃任务的话,可以使用CallerRunsPolicy。该策略不会抛弃任务,也不会抛出异常,而是将任务退回给调用者,使用调用者的线程来执行任务

java 复制代码
public static class CallerRunsPolicy implements RejectedExecutionHandler {
    /**
     * Creates a {@code CallerRunsPolicy}.
     */
    public CallerRunsPolicy() { }

    /**
     * Executes task r in the caller's thread, unless the executor
     * has been shut down, in which case the task is discarded.
     *
     * @param r the runnable task requested to be executed
     * @param e the executor attempting to execute this task
     */
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        if (!e.isShutdown()) {
        //直接主线程执行,不使用线程池中的线程执行。
            r.run();
        }
    }
}

实战举例

线程池常用的阻塞队列

  • 容量为 Integer.MAX_VALUELinkedBlockingQueue(无界队列):FixedThreadPoolSingleThreadExectorFixedThreadPool最多只能创建核心线程数的线程(核心线程数和最大线程数相等),SingleThreadExector只能创建一个线程(核心线程数和最大线程数都是 1),二者的任务队列永远不会被放满。

  • SynchronousQueue(同步队列):CachedThreadPoolSynchronousQueue 没有容量,不存储元素,目的是保证对于提交的任务,如果有空闲线程,则使用空闲线程来处理;否则新建一个线程来处理任务。也就是说,CachedThreadPool 的最大线程数是 Integer.MAX_VALUE ,可以理解为线程数是可以无限扩展的,可能会创建大量线程,从而导致 OOM。

  • DelayedWorkQueue(延迟阻塞队列):ScheduledThreadPoolSingleThreadScheduledExecutorDelayedWorkQueue 的内部元素并不是按照放入的时间排序,而是会按照延迟的时间长短对任务进行排序,内部采用的是"堆"的数据结构,可以保证每次出队的任务都是当前队列中执行时间最靠前的。DelayedWorkQueue 添加元素满了之后会自动扩容原来容量的 1/2,即永远不会阻塞,最大扩容可达 Integer.MAX_VALUE,所以最多只能创建核心线程数的线程。

实战举例

线程池处理任务的流程

通过execute方法,将任务提交给线程池,

  • 如果当前运行的线程数小于核心线程数,那么就会新建一个线程来执行任务。当线程执行完毕,再次提交一个任务时,则会再创建一个新的线程,直到线程池中的核心线程数满为止。

  • 如果当前运行的线程数等于或大于核心线程数,但是小于最大线程数,那么就把该任务放入到任务队列(阻塞队列)里等待执行。当核心线程池中的线程将任务消费之后,则会监听这个阻塞队列,如果阻塞队列不为空,则消费队列里的任务。由于队列是FIFO,所以会先消费最先进入队列的任务。

  • 如果向任务队列投放任务失败(任务队列已经满了),但是当前运行的线程数是小于最大线程数的,就新建一个非核心线程来执行任务。

  • 如果当前运行的线程数已经等同于最大线程数了,新建线程将会使当前运行的线程超出最大线程数,那么当前任务会被拒绝,饱和策略会调用RejectedExecutionHandler.rejectedExecution()方法,对任务进行拦截。

参考文章

  1. JavaGuide面试题
相关推荐
小_太_阳31 分钟前
Scala_【1】概述
开发语言·后端·scala·intellij-idea
智慧老师40 分钟前
Spring基础分析13-Spring Security框架
java·后端·spring
搬码后生仔2 小时前
asp.net core webapi项目中 在生产环境中 进不去swagger
chrome·后端·asp.net
凡人的AI工具箱2 小时前
每天40分玩转Django:Django国际化
数据库·人工智能·后端·python·django·sqlite
Lx3523 小时前
Pandas数据重命名:列名与索引为标题
后端·python·pandas
贵州晓智信息科技3 小时前
如何优化求职简历从模板选择到面试准备
面试·职场和发展
小池先生3 小时前
springboot启动不了 因一个spring-boot-starter-web底下的tomcat-embed-core依赖丢失
java·spring boot·后端
百罹鸟3 小时前
【vue高频面试题—场景篇】:实现一个实时更新的倒计时组件,如何确保倒计时在页面切换时能够正常暂停和恢复?
vue.js·后端·面试
小蜗牛慢慢爬行4 小时前
如何在 Spring Boot 微服务中设置和管理多个数据库
java·数据库·spring boot·后端·微服务·架构·hibernate
wm10435 小时前
java web springboot
java·spring boot·后端