程序员求职经验分享与学习资料整理平台

网站首页 > 文章精选 正文

深度解析Java线程池-别再似懂非懂了

balukai 2025-02-09 11:17:43 文章精选 4 ℃

1、Java线程工厂Executors提供的四种线程池

newCachedThreadPool-缓存线程池,okhttp里使用的是这个

newFixedThreadPool-固定线程数的线程池

newSingleThreadExecutor-单线程线程池

newScheduledThreadPool-实现定时周期性任务的线程池

这四种线程池都直接或间接地获取了ThreadPoolExecutor实例,如果这四种不合适,我们还可以自己创建。ThreadPoolExecutor的构造方法如下,本文解析的重点目标就是搞清楚这个构造方法中的七个参数

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)
复制代码

2、线程池的七个参数

1、corePoolSize:核心线程数

线程池总会维护一个最小的线程数量,即使这些线程处于空闲状态也不会被销毁。除非设置了allowCoreThreadTimeOut。这里的最小线程数量即是corePoolSize。

这里你可能会问,那如果我创建了一个核心线程为3的线程池,线程池一创建就会有3个线程吗?

并不是的,默认情况下并不会创建出来核心线程,是在任务来到时,才创建的。并且在线程数少于核心线程数时,任务到来,无论当前的线程是否处于空闲状态,都会创建线程。

接着又可能会疑问,创建线程相对而言耗时还是比较大,如果我想在线程池创建时就把核心线程创建好呢?我就是傲娇,我就是要这么干呢?

办法还是有的,重写prestartCoreThread方法实现预启动核心线程或者调用prestartAllCoreThreads方法实现

2、maximumPoolSize:线程池最大线程数量

一个任务被提交到线程池后,首先看有没有空闲线程,如果有直接执行任务。如果没有,会缓存到工作队列,如果工作队列满了,则会创建新线程,然后从工作队列中取出一个任务由新线程来处理,而刚提交的任务放入工作队列。线程池不会无限制地创建新线程,它会有一个最大线程数量的限制,这个数量由maximumPoolSize来指定

3、keepAliveTime:空闲线程存活时间

一个线程如果处于空闲状态,并且当前线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁。这里的指定时间由keepAliveTime来指定

这里你可能会问, 我想对核心线程也来个存活时间呢?

可以通过设置allowCoreThreadTimeOut来实现。但是注意,只在keepAliveTime不等于0时才会生效。也就是说非核心线程可以用完即释放,但是核心线程不行

这里对核心线程和非核心线程的待遇就不同了。核心线程好比公司正牌员工,非核心线程是活多的时候招过来的临时工,活干完了,就把正牌员工解雇(卸磨杀驴不合适吧),临时工就不一样了,原本就是临时的~

4、unit:空闲线程存活时间单位

5、workQueue:工作队列-java里提供了四种阻塞队列

(1)、ArrayBlockingQueue

基于数组的有界阻塞队列,按FIFO排序。当线程池中的线程数量达到corePoolSize后,再有新任务进来,会将任务放到队尾,等待调度。如果队列已经是满的,则会创建新线程。如果线程数达到了maximumPoolSize,则会执行拒绝策略

(2)、LinkedBlockingQueue

基于链表的无界阻塞队列(默认情况下)。按FIFO排序。该队列近似无界。当线程池中线程数量达到corePoolSize后,再有新任务进来,会一直存入该队列。而不会去创建新线程。因此这种情况下的maximumPoolSize实际上是不起作用的

(3)、SynchronousQueue

一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说,线程数量达到corePoolSize后,新任务到来,会创建新线程。如果线程数量达到maxPoolSize,则会执行拒绝策略

(4)、PriorityBlockQueue

具有优先级的无界阻塞队列,优先级通过参数Comparator实现

都是阻塞队列,那阻塞队列和普通队列有什么区别呢?

阻塞队列是一个在队列基础上又支持了两个附加操作的队列。

2个附加操作:

支持阻塞的插入方法:队列满时,队列会阻塞插入元素的线程,直到队列不满。

支持阻塞的移除方法:队列空时,获取元素的线程会等待队列变为非空。

6、threadFactory线程工厂

创建一个新线程时使用的工厂,可以用来指定线程名,是否为daemon线程等

7、handle拒绝策略

当工作队列中的任务达到最大限制,并且线程池中的线程数量也达到最大限制,新任务到来,会执行拒绝策略。java中提供了4拒绝策略

(1)、CallerRunsPolicy

在该策略下,在调用者线程中直接执行被拒绝任务的run方法,除非线程池已经shotdown,则直接抛弃任务

(2)、AbortPolicy

该策略下,直接丢弃任务,并抛出
RejectedExecutionException异常

(3)、DiscardPolicy

在该策略下,直接丢弃任务,什么都不做。

(4)、DiscardOldestPolicy

该策略下,抛弃进入队列最早的任务,然后尝试把这次拒绝的任务加入队列

3、小结一下

1、线程池运行整体流程

在不做特殊设置的情况下,比如上文提到的几类设置(预先创建核心线程、给核心线程设置存活时间等)

线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。

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

1.如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务

2.如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列。

3.如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建线程运行这个任务;

4.如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常,告诉调用者“我不能再接受任务了”。

5.当一个线程完成任务时,它会从队列中取下一个任务来执行。

6.当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。

2、案例:先加入任务就一定会先执行吗?

假设队列大小为 10,corePoolSize 为 3,maximumPoolSize 为 6,那么当加入 20 个任务时,执行的顺序就是这样的:首先执行任务 1、2、3,然后任务 4-13 被放入队列。这时候队列满了,任务 14、15、16 会被马上执行,而任务 17-20 则会执行拒绝策略。最终顺序是:1、2、3、14、15、16、4、5、6、7、8、9、10、11、12、13。

3、聊聊排队这件事

1、不排队,一个不错的默认选择

使用SynchronousQueue+无限大的maximumPoolSize。上文提到SynchronousQueue是一个不缓存的队列,每来一个任务如果没达到maximumPoolSize,就会有线程去执行它(创建或复用),无需排队。举个案例:newCachedThreadPool的实现

public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue());
    }
复制代码

newCachedThreadPool(缓存线程池),使用了SynchronousQueue+无限大的maximumPoolSize组合(Integer.MAX_VALUE可近似看成无限大了)。

同时核心线程还为0,让所有线程都遵循了60秒的空闲线程存活时间规则。

对于执行数量多的短期异步任务,有提高性能的作用,当任务到来时,如果没有可用的空闲线程,就创建线程添加到池子里。而在足够长的闲置时间后,将全部释放不消耗资源。

2、排长龙。

使用LinkedBlockingQueue+有限线程数(最大线程数设置和核心线程一样)。当任务到来,如果没有空闲可用的线程了,就排到队列里,无限大的队列防止任务丢失。实际上newFixedTheradPool和newSingleThreadExecutor是这么实现的

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue());
    }
复制代码
public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue()));
    }
复制代码

3、控制排队,多租点地盘-高端操作

使用有界队列ArrayBlockingQueue+控制maximumPoolSize的大小。如果是任务多,耗时短,就采用大队列+小的线程数,降低cpu使用率、操作系统资源和上下文切换消耗。当出现任务耗时时间长,经常阻塞,那就用更大的池来避免阻塞。

原文链接:
https://juejin.cn/post/6907833986316238856

如果觉得本文对你有帮助,可以转发关注支持一下

最近发表
标签列表