随笔 - 171  文章 - 0  评论 - 0  阅读 - 62432

线程池

线程池的生命周期,总共有五种状态

  • RUNNING :能接受新提交的任务,并且也能处理任务队列中的任务;
  • SHUTDOWN:关闭状态,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务。在线程池处于 RUNNING 状态时,调用 shutdown()方法会使线程池进入到该状态。(finalize() 方法在执行过程中也会调用shutdown()方法进入该状态);
  • STOP:不能接受新任务,也不处理队列中的任务,会中断正在处理任务的线程。在线程池处于 RUNNING 或 SHUTDOWN 状态时,调用 shutdownNow() 方法会使线程池进入到该状态;
  • TIDYING:如果所有的任务都已终止了,workerCount (有效线程数) 为0,线程池进入该状态后会调用 terminated() 方法进入TERMINATED 状态。
  • TERMINATED:在terminated() 方法执行完后进入该状态,默认terminated()方法中什么也没有做。

为什么要用线程池

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

ThreadPoolExecutor线程池类参数详解

参数

说明
corePoolSize                                                                      
核心线程数量,线程池维护线程的最少数量
maximumPoolSize
线程池维护线程的最大数量
keepAliveTime
线程池除核心线程外的其他线程的最长空闲时间,超过该时间的空闲线程会被销毁
unit
keepAliveTime的单位,TimeUnit中的几个静态属性:NANOSECONDS、MICROSECONDS、MILLISECONDS、SECONDS
workQueue
线程池所使用的任务缓冲队列
threadFactory
线程工厂,用于创建线程,可以规范线程名称

handler

线程池对拒绝任务的处理策略

当线程池任务处理不过来的时候(什么时候认为处理不过来后面描述),可以通过handler指定的策略进行处理,ThreadPoolExecutor提供了四种策略:

  1. ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常;也是默认的处理方式。
  2. ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。
  3. ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)。
  4. ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务。谁提交任务谁就要负责执行任务,这样提交任务的线程就得负责执行任务,而执行任务又是比较耗时的,在这段期间,提交任务的线程被占用,也就不会再提交新的任务,减缓了任务提交的速度,相当于是一个负反馈。在此期间,线程池中的线程也可以充分利用这段时间来执行掉一部分任务,腾出一定的空间,相当于是给了线程池一定的缓冲期。
可以通过实现RejectedExecutionHandler接口自定义处理方式。

核心线程数一般配置多少?

根据线程池执行的任务特性设置,有条件最好根据压测结果调整线程数量。

CPU密集型任务

  • CPU密集型任务也叫计算密集型任务。

  • 比如加密、解密、压缩、计算等一系列需要大量耗费 CPU 资源的任务。

  • CPU密集型任务最佳的线程数为 CPU 核心数的 1~2 倍。

  • 如果设置过多的线程数,实际上并不会起到很好的效果。

  • 假设我们设置的线程数量是 CPU 核心数的 2 倍以上,因为计算任务非常重,会占用大量的 CPU 资源,所以这时 CPU 的每个核心工作基本都是满负荷的。

  • 我们又设置了过多的线程,每个线程都想去利用 CPU 资源来执行自己的任务,这就会造成不必要的上下文切换,此时线程数的增多并没有让性能提升,反而由于线程数量过多会导致性能下降。

IO密集型任务

  • 比如数据库、文件的读写,网络通信等任务。

  • 并不会特别消耗 CPU 资源,但是 IO 操作很耗时,总体会占用比较多的时间。

  • IO密集型任务最大线程数一般会大于 CPU 核心数很多倍,因为 IO 读写速度相比于 CPU 的速度而言是比较慢的,如果我们设置过少的线程数,就可能导致 CPU 资源的浪费。

  • 如果我们设置更多的线程数,那么当一部分线程正在等待 IO 的时候,它们此时并不需要 CPU 来计算,那么另外的线程便可以利用 CPU 去执行其他的任务,互不影响,这样的话在工作队列中等待的任务就会减少,可以更好地利用资源。

  • 《Java并发编程实战》的作者 Brain Goetz 推荐的计算方法:线程数 = CPU 核心数 *(1+线程执行任务时CPU等待时间/线程占有CPU工作时间)。通过这个公式,我们可以计算出一个合理的线程数量,如果任务的平均等待时间长,线程数就随之增加,而如果平均工作时间长,也就是对于我们上面的 CPU 密集型任务,线程数就随之减少。

周期性线程池

ScheduledThreadPoolExecutor
两种方式:
复制代码
/**
  command参数是任务实例,
  initialDelay参数是初始换延迟时间,
  delay参数是延迟间隔时间,
  unit参数是时间单元
**/
scheduleAtFixedRate(Runnable command,
                                 long initialDelay,
                                  long period,
                                  TimeUnit unit)
scheduleWithFixedDelay(Runnable command,
                                 long initialDelay,
                                 long delay,
                                 TimeUnit unit)
复制代码
  • scheduleWithFixedDelay固定执行间隔时间,从执行结束计算间隔时间。
  • scheduleAtRate固定执行频率,从执行开始计算间隔时间。
getTask通过DelayedWorkQueue获取任务,取任务时判断是否到了执行时间。执行完了将任务重新放进小顶堆中,根据任务延时时间进行排序,队首元素是延迟时间最短的元素。

线程池任务执行

  • submit() 该方法返回一个Future对象,可执行带返回值的线程;或者执行想随时可以取消的线程。Future对象的get()方法获取返回值。Future对象的cancel(true/false)取消任务,未开始或已完成返回false,参数表示是否中断执行中的线程。
  • execute() 没有返回值。

线程池任务提交过程

一个线程提交到线程池的处理流程如下图:
  1. 如果此时线程池中的数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
  2. 如果此时线程池中的数量等于corePoolSize,但是缓冲队列workQueue未满,那么任务被放入缓冲队列。
  3. 如果此时线程池中的数量大于等于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的线程来处理被添加的任务。
  4. 如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。
  5. 当线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数。
总结即:处理任务判断的优先级为核心线程corePoolSize>任务队列workQueue>最大线程maximumPoolSize,如果三者都满了,使用handler处理被拒绝的任务。
注意:
  1. 当workQueue使用的是无界限队列时,maximumPoolSize参数就变的无意义了,比如new LinkedBlockingQueue(),或者new ArrayBlockingQueue(Integer.MAX_VALUE)。
  2. 使用SynchronousQueue队列时由于该队列没有容量的特性,所以不会对任务进行排队,如果线程池中没有空闲线程,会立即创建一个新线程来接收这个任务。maximumPoolSize要设置大一点。
  3. 核心线程和最大线程数量相等时keepAliveTime无作用。

执行源码

线程池关闭

  1. shutdown() 不接收新任务,会处理已添加任务
  2. shutdownNow() 不接受新任务,不处理已添加任务,中断正在处理的任务

常用队列介绍

  1. ArrayBlockingQueue: 这是一个由数组实现的容量固定的有界阻塞队列。
  2. SynchronousQueue: 没有容量,不能缓存数据;每个put必须等待一个take; offer()的时候如果没有另一个线程在poll()或者take()的话返回false。
  3. LinkedBlockingQueue: 这是一个由单链表实现的默认无界的阻塞队列。LinkedBlockingQueue提供了一个可选有界的构造函数,而在未指明容量时,容量默认为Integer.MAX_VALUE。
队列操作:
方法
说明
add                              
增加一个元索; 如果队列已满,则抛出一个异常
remove
移除并返回队列头部的元素; 如果队列为空,则抛出一个异常
offer
添加一个元素并返回true; 如果队列已满,则返回false
poll
移除并返回队列头部的元素; 如果队列为空,则返回null
put
添加一个元素; 如果队列满,则阻塞
take
移除并返回队列头部的元素; 如果队列为空,则阻塞
element
返回队列头部的元素; 如果队列为空,则抛出一个异常
peek
返回队列头部的元素; 如果队列为空,则返回null

Executors线程工厂类

  1. Executors.newCachedThreadPool();
说明: 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程.
内部实现:new ThreadPoolExecutor(0,Integer.MAX_VALUE,60L,TimeUnit.SECONDS,new SynchronousQueue());
  1. Executors.newFixedThreadPool(int)

说明: 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

内部实现:new ThreadPoolExecutor(nThreads, nThreads,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue());

  1. Executors.newSingleThreadExecutor();
说明:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照顺序执行。
内部实现:new ThreadPoolExecutor(1,1,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue())
  1. Executors.newScheduledThreadPool(int);
说明:创建一个定长线程池,支持定时及周期性任务执行。
内部实现:new ScheduledThreadPoolExecutor(corePoolSize)。

阿里巴巴Java开发手册中对线程池的使用规范

  • 【强制】创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。
    正例:
    public class TimerTaskThread extends Thread {
        public TimerTaskThread(){
            super.setName("TimerTaskThread"); 
            ...
        }
    }
  • 【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
说明: 使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开销,解决资
源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者
“过度切换”的问题。
  • 【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明: Executors 返回的线程池对象的弊端如下:
1) FixedThreadPool 和 SingleThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
2) CachedThreadPool 和 ScheduledThreadPool:
允许的创建线程数量为 Integer.MAX_VALUE, 可能会创建大量的线程,从而导致 OOM。

优雅关闭

利用JVM钩子函数,在虚拟机关闭时调用相关方法即”优雅关闭线程池”。Runtime.getRuntime().addShutdownHook(),多个线程池进行循环添加钩子,每个钩子会并发调用。
ApplicationShutdownHooks
复制代码
    /* Iterates over all application hooks creating a new thread for each
     * to run in. Hooks are run concurrently and this method waits for
     * them to finish.
     */
    static void runHooks() {
        Collection<Thread> threads;
        synchronized(ApplicationShutdownHooks.class) {
            threads = hooks.keySet();
            hooks = null;
        }

        for (Thread hook : threads) {
            hook.start();
        }
        for (Thread hook : threads) {
            while (true) {
                try {
                    hook.join();
                    break;
                } catch (InterruptedException ignored) {
                }
            }
        }
    }
}
复制代码
比如最多等待5分钟,先调shutdown,awaitTermination一半时间2.5min,如果返回值是false,表示线程池还未关闭,那么调用shutdownNow,继续awaitTermination一半时间。最后调用isTerminated(),如果线程池状态还未销毁,进行报警监控记录。
复制代码
// com.google.common.util.concurrent.MoreExecutors#shutdownAndAwaitTermination
public static boolean shutdownAndAwaitTermination(ExecutorService service, long timeout, TimeUnit unit) {
    // 把阻塞时间平分
    long halfTimeoutNanos = unit.toNanos(timeout) / 2;
    // Disable new tasks from being submitted
    // 这一步是必做的,保证线程池不再接受新的任务
    service.shutdown();
    try {
        // Wait for half the duration of the timeout for existing tasks to terminate
        // 先最多等设置的阻塞时间的一半时间
        if (!service.awaitTermination(halfTimeoutNanos, TimeUnit.NANOSECONDS)) {
            // Cancel currently executing tasks
            // 考虑到有些任务可能处于阻塞状态,直接 shutdownNow,会中断执行的任务
            service.shutdownNow();
            // Wait the other half of the timeout for tasks to respond to being cancelled
            service.awaitTermination(halfTimeoutNanos, TimeUnit.NANOSECONDS);
        }
    } catch (InterruptedException ie) {
        // Preserve interrupt status
        Thread.currentThread().interrupt();
        // (Re-)Cancel if current thread also interrupted
        service.shutdownNow();
    }
    return service.isTerminated();
}
复制代码
posted on   zhengbiyu  阅读(16)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?
< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5

点击右上角即可分享
微信分享提示