• JAVA线程池的使用


    一、使用 Executors 创建线程池

    Executors是一个线程池工厂类,里面有许多静态方法,供开发者调用。

    /* 该方法返回一个固定线程数量的线程池,该线程池池中的线程数量始终不变。
    * 当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。
    * 若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务
    * 默认等待队列长度为Integer.MAX_VALUE
    */
    ExecutorService fixedThreadPool = Executors.newFixedThreadPool(1);
    /* 该方法返回一个只有一个线程的线程池。
    * 若多余一个任务被提交到线程池,任务会被保存在一个任务队列中,等待线程空闲,按先入先出顺序执行队列中的任务
    * 默认等待队列长度为Integer.MAX_VALUE
    */
    ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
    /*
    * 该方法返回一个可根据实际情况调整线程数量的线程池。
    * 线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。
    * 若所有线程均在工作,又有新任务的提交,则会创建新的线程处理任务。
    * 所有线程在当前任务执行完毕后,将返回线程池进行复用
    */
    ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
    /* 该方法返回一个ScheduledExecutorService对象,线程池大小为1。
    * ScheduledExecutorService接口在ExecutorService接口之上扩展了在给定时间内执行某任务的功能,
    * 如在某个固定的延时之后执行,或者周期性执行某个任务
    */
    ExecutorService newSingleThreadScheduledExecutor = Executors.newSingleThreadScheduledExecutor();
    /*
    * 该方法也返回一个ScheduledExecutorService对象,但该线程池可以指定线程数量
    */
    ExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(1);

    Executors 的静态方法都是基于 ThreadPoolExecutor 类实现的,相当于 ThreadPoolExecutor 的语法糖。

    但这几个静态方法都存在一个弊端,因为会在创建线程池的同时隐式创建等待队列,而队列的长度默认是 Integer.MAX_VALUE ,相当于不限长度,这样就存在OOM的隐患。

    二、使用 ThreadPoolExecutor 创建线程池

    上面说过,Executors 的静态方法都是基于 ThreadPoolExecutor 类实现的,所以在生产环境下,还是建议直接使用 ThreadPoolExecutor 类创建线程池:

    public ThreadPoolExecutor(int corePoolSize,
    int maximumPoolSize,
    long keepAliveTime,
    TimeUnit unit,
    BlockingQueue<Runnable> workQueue);

    ThreadPoolExecutor 有多个构造方法,一般来说使用最精简的即可。

    三、参数含义

    corePoolSize

    指定线程池的核心线程数。

    当一个新任务被添加到线程池时,首先会判断当前的线程数(ThreadCount),如果:

    A:ThreadCount < corePoolSize:即当前线程数小于核心线程数,就会创建一个新的线程来执行这个任务;

    B:ThreadCount >= corePoolSize:即当前线程数大于等于核心线程数,就会将新任务添加到等待队列中。

    该参数的两个特殊参数值:

    1、0:意味着没有核心线程,全部线程都会受到 keepAliveTime 参数的回收机制影响。

    2、Integer.MAX_VALUE:意味着不限制核心线程数,连等待队列都不需要,可以想象这种情况下很容易OOM。

    maximumPoolSize

    指定线程池的最大线程数,包括核心线程和非核心线程。

    当另一个新任务被添加到线程池时,如果此时等待队列的容量已满,则会判断当前的线程数(ThreadCount),如果:

    A:ThreadCount < maximumPoolSize:即当前线程数小于最大线程数,就会创建一个新的线程来执行这个任务;

    B:ThreadCount == maximumPoolSize:即当前线程数已达到最大值,此时等待队列的容量也已用尽,因此会抛出异常。

    该参数的两个特殊参数值:

    1、0:意味着只有核心线程,默认情况下全部线程都不会受到 keepAliveTime 参数的回收机制影响,除非设置 allowCoreThreadTimeOut 为 true。

    2、Integer.MAX_VALUE:意味着不限制最大线程数,这种情况下也很容易OOM。

    keepAliveTime

    空闲线程的存活时间。

    默认情况下,该参数只对非核心线程有效。

    在处理大量任务时,可能会创建大量的非核心线程,在所有任务都执行完成后会继续保留这些非核心线程一段时间,等时间到了就会自动回收,以减少系统开销。

    当设置线程池的 allowCoreThreadTimeOut(true) 时,意味着该参数也同时对核心线程有效,在时间到了之后,全部线程都会自动回收。

    unit

    空闲线程存活时间的单位。

    workQueue

    等待队列。

    创建线程池时另外一个容易引起OOM的重要参数,主要包括以下几种:

    1、ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
    2、LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按 FIFO(先进先出)排序元素,吞吐量通常要高于 ArrayBlockingQueue。静态工厂方法 Executors.newFixedThreadPool() 使用了这个队列。
    3、SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于 LinkedBlockingQueue,静态工厂方法 Executors.newCachedThreadPool 使用了这个队列。
    4、PriorityBlockingQueue:一个具有优先级的无限阻塞队列。

     

    以最常用的 LinkedBlockingQueue 为例:

    //创建一个容量为9999的队列实例
    BlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>(9999);

     

    关于线程池各参数的作用,可以通过下面的图片进行详细了解:

    四、使用线程池的注意事项

    一句话:应该最大化的,同时也要有限度的满足业务需求。

    在实际使用线程池时,首先应该确保所创建的线程池可以满足业务设计需求,主要就是线程数和队列容量,前者由CPU核心数限制,后者由服务器内存限制。

    线程太少,则消费队列的时间就长,就需要更大容量的队列;线程太多,会增加大量的上下文切换时间,反而不利于合理分配CPU的计算资源。

    队列太小,则添加任务时可能会抛出异常;队列太大,会占用更多的内存消耗。

    关键是切勿使用无边界值(Integer.MAX_VALUE),这也是造成OOM的最主要原因。

    可以根据服务器配置和业务需求,对这两个方面进行均衡考虑。

    五、使用案例

    int cpuCoreCnt = Runtime.getRuntime().availableProcessors(); //获取服务器CPU核心数
    int corePoolSize = cpuCoreCnt; // 核心线程数
    int maximumPoolSize = cpuCoreCnt; // 最大线程数
    int keepAliveTime = 30; // 非核心线程的空闲存活时长(分钟)
    int queueCapacity = 9999; // 队列最大长度
    BlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>(queueCapacity);
    ThreadPoolExecutor threadPool = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.MINUTES, queue);
    threadPool.allowCoreThreadTimeOut(true); //允许回收核心线程

    上面案例中,使用CPU核心数作为最大线程数,相对来说还是比较合理的。

    等待队列的容量尽可能设置的大一些,和添加任务时抛出异常相比,多付出一些内存来实现更大容量的队列还是非常值得的。

    keepAliveTime 也可以适当设置的长一些,避免太快回收,毕竟频繁的创建线程也是需要时间开销的。

    最后还设置了allowCoreThreadTimeOut方法,允许自动回收核心线程,用来减少阻塞线程的性能消耗。

    六、线程池复用

    线程池在完成全部的任务后,会自动进入摸鱼状态,期间会根据配置自动回收空闲线程,直到新的任务被添加进来再起来工作。

    即使设置了 allowCoreThreadTimeOut(true) 对核心线程进行回收,有新任务时也会重新创建核心线程继续进入工作状态。

    只要不是调用 shutdown() 手动关闭它,正常情况下线程池是可以长期重复性使用的。

    有些强迫症患者(比如本人)会非常介意一个无所事事的线程池在内存里装死,因此必须手动 shutdown 才会安心。

    但这样的话,之前的线程池就彻底挂掉了,再向其中添加任务时会抛出异常。

    有效的做法是,将创建线程池的部分单独封装,每次添加任务时都进行判断,如果当前线程池已经挂掉了,就重新创建一个:

    /**
    * <p>
    * 添加任务
    * 注:如果线程池已关闭,会自动创建新的线程池
    * </p>
    *
    * @param task
    */
    public void addTask(Task task){
    if(threadPool.isShutdown()) createThreadPool(corePoolSize, maximumPoolSize, keepAliveTime);
    threadPool.execute(task);
    }
  • 相关阅读:
    黑客入门指南,学习黑客必须掌握的技术
    最短路算法总结
    opencv 图像平滑
    Docker容器与虚拟化技术:DaoCloud账户注册
    拥抱 Spring 全新 OAuth 解决方案
    3D Gaussian Splatting for Real-Time Radiance Field Rendering(慢慢啃,还是挺复杂的)
    「 程序员的理财与风险控制」现金流管理:教育年金和养老年金
    SpringBoot通过配置切换注册中心(多注册中心nacos和eureka)
    linux 内核模块重定位过程 简单的示例解析
    如何注册意大利公司 意大利公司公证 意大利公司注册流程
  • 原文地址:https://www.cnblogs.com/netWild/p/16079369.html