exector:ThreadPoolExecutor 类介绍 $
ThreadPoolExecutor 类是Excutor的底层实现
构造方法(4个):
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.corePoolSize = corePoolSize; this.maximumPoolSize = maximumPoolSize; this.workQueue = workQueue; this.keepAliveTime = unit.toNanos(keepAliveTime); this.threadFactory = threadFactory; this.handler = handler; }
corePoolSize:核心线程数,任务队列未达到队列容量时,最大可以同时运行的线程数量
maxinumPoolSize:最大线程数,任务队列达到队列容量时,最大可以同时运行的线程数量变为最大线程数。
workQueue:当前线程数超过corePoolSize,线程被放入workQueue
unit:时间单位
keepAtiveTime:其余线程可存活的最长时间
threadFactory:线程工厂,用来executer创建线程
handler: 饱和策略(拒绝策略)
ThreadPoolExecuter饱和策略定义:当前同时运行的线程数量达到最大线程数,且队列已经被放满
,
ThreadPoolTaskExecutor
定义一些策略:
- AbortPolicy:直接抛出异常,
RejectedExecutionException,
拒绝新任务的处理。(spring默认) - CallerRunsPolicy:任务的执行由注入的线程自己执行,就是直接在调用
execute
方法的线程中运行(run
)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。- 因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略
。(可伸缩队列)
- 因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略
- DiscardPolicy:不处理新任务,直接丢弃掉。
- DiscardOldPolicy:丢弃最早的未处理的任务请求。
2.通过Executer框架的工具类Executors创建
newFixedThreadPool------> 创建固定线程数量的线程池,可控制线程最大并发数,超出的线程会在队列中等待。 适用于任务量已知,相对耗时的任务。核心线程数=最大线程数,无需设超时时间,阻塞队列是无界的LinkedBlockingQueue,可以放任意数量的任务。 newCachedThreadPool-----> 创建一个可缓存线程池,创建一个线程池,该线程池会根据需要创建新的线程,但如果之前创建的线程可以使用,会重用之前创建的线程. 核心线程数=0,Integer.MAX_VALUE,救急线程生存时间是60s,说明全部都是救急线程,救急线程也可以无限创建,队列SynchronousQueue没有容量,线程来取才能放进队列任务。 适用于任务数比较密集,但是每个任务执行时间较短的情况。 newScheduledThreadPool----> 创建固定线程数量的线程池,支持定时及周期性任务执行。 用来执行延迟或反复执行的周期性任务。 newSingleThreadExecutor-----> 创建一个单线程化的线程池,它会只用唯一的工作线程来执行任务,保证所有任务按照指定顺序FIFO、LIFO,优先级执行。 希望多个任务排队执行,和单线程相比的区别在于,这个死了一个线程,会重新创建一个新的线程消费后续任务,可用性更高
Executors
返回线程池对象的弊端如下:
-
FixedThreadPool
和SingleThreadExecutor
: 使用的是无界的LinkedBlockingQueue
,任务队列最大长度为Integer.MAX_VALUE
,可能堆积大量的请求,从而导致 OOM。 -
CachedThreadPool
:使用的是同步队列SynchronousQueue
, 允许创建的线程数量为Integer.MAX_VALUE
,可能会创建大量线程,从而导致 OOM。 -
ScheduledThreadPool
和SingleThreadScheduledExecutor
: 使用的无界的延迟阻塞队列DelayedWorkQueue
,任务队列最大长度为Integer.MAX_VALUE
,可能堆积大量的请求,从而导致 OOM。
线程池常用的阻塞队列总结:
新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。LinkedBlockingQueue
(无界队列):FixedThreadPool
和 SingleThreadExector
。容量为 Integer.MAX_VALUE,
由于队列永远不会被放满,因此FixedThreadPool
最多只能创建核心线程数的线程。 SynchronousQueue
(同步队列) :CachedThreadPool
。SynchronousQueue
没有容量,不存储元素,目的是保证对于提交的任务,如果有空闲线程,则使用空闲线程来处理;否则新建一个线程来处理任务。也就是说,CachedThreadPool
的最大线程数是 Integer.MAX_VALUE
,可以理解为线程数是可以无限扩展的,可能会创建大量线程,从而导致 OOM。 DelayedWorkQueue
(延迟阻塞队列):ScheduledThreadPool
和 SingleThreadScheduledExecutor
。DelayedWorkQueue
的内部元素并不是按照放入的时间排序,而是会按照延迟的时间长短对任务进行排序,内部采用的是“堆”的数据结构,可以保证每次出队的任务都是当前队列中执行时间最靠前的。DelayedWorkQueue
添加元素满了之后会自动扩容原来容量的 1/2,即永远不会阻塞,最大扩容可达 Integer.MAX_VALUE
,所以最多只能创建核心线程数的线程。
FixedThreadPool是因为阻塞队列可以很大(最大为Integer最大值),故几乎不会触发拒绝策略;
CachedThreadPool是因为线程池很大(最大为Integer最大值),几乎不会导致线程数量大于最大线程数,故几乎不会触发拒绝策略。
PriorityBlockingQueue 是一个支持优先级的无界阻塞队列,可以看作是线程安全的 PriorityQueue,两者底层都是使用小顶堆形式的二叉堆,即值最小的元素优先出队。不过,PriorityQueue 不支持阻塞操作。
要想让 PriorityBlockingQueue 实现对任务的排序,传入其中的任务必须是具备排序能力的,方式有两种:
提交到线程池的任务实现 Comparable 接口,并重写 compareTo 方法来指定任务之间的优先级比较规则。
创建 PriorityBlockingQueue 时传入一个 Comparator 对象来指定任务之间的排序规则(推荐)。
ThreadPoolExecutor的submit、invokeXxx、execute方法入参都是Runnable、Callable,均不具备可排序的属性。可以弄一个实现类 PriorityTask,加一些额外的属性,让它们具备排序能力。
实现Runnable接口,作为任务和存储在优先级队列中的Comparable接口。此类包含用于存储任务优先级的Priority属性,属性值高的任务将先被执行,compareTo()方法在优先级队列中确定任务的排序。 在Main类中,发送20个具有不同优先级的任务执行器,首个发送到执行器的任务将被第一个执行。当执行器空闲时,任务只要到达就会立即执行第一个任务。 实现声明在Comparable接口中的compareTo()方法。它将PriorityTask对象作为参数接收,并比较当前对象和作为参数的对象的优先级。设置高优先级任务在低优先级任务之前执行: @Override public int compareTo(MyPriorityTask o) { return Integer.compare(o.getPriority(), this.getPriority()); }
存在一些风险和问题,比如:
PriorityBlockingQueue 是无界的,可能堆积大量的请求,从而导致 OOM。可能会导致饥饿问题,即低优先级的任务长时间得不到执行。
由于需要对队列中的元素进行排序操作以及保证线程安全(并发控制采用的是可重入锁 ReentrantLock),因此会降低性能。
对于 OOM 这个问题的解决比较简单粗暴,就是继承PriorityBlockingQueue 并重写一下 offer 方法(入队)的逻辑,当插入的元素数量超过指定值就返回 false 。
饥饿问题这个可以通过优化设计来解决(比较麻烦),比如等待时间过长的任务会被移除并重新添加到队列中,但是优先级会被提升。
对于性能方面的影响,是没办法避免的,毕竟需要对任务进行排序操作。并且,对于大部分业务场景来说,这点性能影响是可以接受的。
队列选取:
功能性:比如延时,排序等功能,如果用到排序功能,这个时候就需要考虑PriorityBlockingQueue队列。
容量:如果队列有存储的要求或者是直接传递的要求,就要选择不同的阻塞队列。
有固定容量的队列 ArrayBlockingQueue;
默认是无限容量的队列 LinkedBlockingQueue;
有的队列没有任何容量的,如同步队列SynchronousQueue;
DelayQueue的容量为Integer.MAX_VALUE,不同的队列之间千差万别,我们需要根据任务数量来推算出合适的队列。
能否扩容:从扩容方面考虑 ,在开发过程中,对于一些业务我们没有办法估计他的容量,业务可能有高峰期和低谷期,开一个固定容量无法满足业务需求,这个时候需要选择动态扩容。
所以 ArrayBlockingQueue是不可以使用的,他在创建时容量就固定了,无法扩容。PriorityBlockingQueue 即使在指定了初始容量之后,后续如果有需要,也可以自动扩容。所以 我们可以根据是否需要扩容来选取合适的队列。
内存结构:队列的内存结构分为两种,一种是数据结构的,如ArrayBlockingQueue,还有一种是链表结构的,如LinkedBlockingQueue,ArrayBlockingQueue 没有链表所需要的“节点”,空间利用率更高。
性能:比如 LinkedBlockingQueue 由于拥有两把锁,它的操作粒度更细,在并发程度高的时候,相对于只有一把锁的 ArrayBlockingQueue 性能会更好。
另外,SynchronousQueue 性能往往优于其他实现,因为它只需要“直接传递”,而不需要存储的过程。如果我们的场景需要直接传递的话,可以优先考虑 SynchronousQueue。
线程池原理分析:
线程池会先执行corePoolSize数量的线程,这里是五个。然后这些任务有任务被执行完的话,就会去拿新的任务执行。
for(int i=0;i<10;i++){ Runnable worker=new MyRunnable(""+i); executor.execute(worker); }
线程池原理分析(面试回答):
线程池实现类 ThreadPoolExecutor
是 Executor
框架最核心的类。
1.线程池创建 ThreadPoolExecutor 七个参数
2.线程池执行任务的流程:executor.execute();
3.在 execute
方法中,多次调用 addWorker
方法。addWorker
这个方法主要用来创建新的工作线程,如果返回 true 说明创建和启动工作线程成功,否则的话返回的就是 false。
整个流程(对整个逻辑进行了简化,方便理解):
1.如果当前运行的线程数小于核心线程数,那么就会新建一个线程来执行任务。
2.如果当前运行的线程数等于或大于核心线程数,但是小于最大线程数,那么就把该任务放入到任务队列里等待执行。
3.如果向任务队列投放任务失败(任务队列已经满了),但是当前运行的线程数是小于最大线程数的,就新建一个线程来执行任务。
4.如果当前运行的线程数已经等同于最大线程数了,新建线程将会使当前运行的线程超出最大线程数,那么当前任务会被拒绝,饱和策略会调RejectedExecutionHandler.rejectedExecution()
方法。
线程数怎么选择:
CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1。
比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。
如何判断是 CPU 密集任务还是 IO 密集任务?
CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序。
但凡涉及到网络读取,文件读取这类都是 IO 密集型,这类任务的特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上。
几个常见对比:
1.Runnable
vs Callable
Runnable
自 Java 1.0 以来一直存在,但Callable
仅在 Java 1.5 中引入。
Runnable
接口不会返回结果或抛出检查异常,但是 Callable
接口可以。
所以,如果任务不需要返回结果或抛出异常推荐使用 Runnable
接口,这样代码看起来会更加简洁。
工具类 Executors
可以实现将 Runnable
对象转换成 Callable
对象。(Executors.callable(Runnable task)
或 Executors.callable(Runnable task, Object result)
)
@FunctionalInterface public interface Runnable { /** * 被线程执行,没有返回值也无法抛出异常 */ public abstract void run(); @FunctionalInterface public interface Callable<V> { /** * 计算结果,或在无法这样做时抛出异常。 * @return 计算得出的结果 * @throws 如果无法计算结果,则抛出异常 */ V call() throws Exception; }
2.execute()
vs submit()
·execute()
方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
·
submit()
方法用于提交需要返回值的任务。线程池会返回一个 Future
类型的对象,通过这个 Future
对象可以判断任务是否执行成功,并且可以通过 Future
的 get()
方法来获取返回值,get()
方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)
方法的话,如果在 timeout
时间内任务还没有执行完,就会抛出 java.util.concurrent.TimeoutException
。
Future<String> future= executorService.submit(() -> { try { Thread.sleep(5000L); } catch (InterruptedException e) { e.printStackTrace(); } return "abc"; }); String s = future.get(); String s = future.get(3, TimeUnit.SECONDS);
3.shutdown()
VSshutdownNow()
·shutdown()
:关闭线程池,线程池的状态变为 SHUTDOWN
。线程池不再接受新任务了,但是队列里的任务得执行完毕。
·shutdownNow()
:关闭线程池,线程的状态变为 STOP
。线程池会终止当前正在运行的任务,并停止处理排队的任务并返回正在等待执行的 List。
4.isTerminated()
VS isShutdown()
·isShutDown
当调用 shutdown()
方法后返回为 true。
·isTerminated
当调用 shutdown()
方法后,并且所有提交的任务完成后返回为 true
几个常见内置线程池:(有空补充)
1.FixedThreadPool
2.SingleThreadPool
3.CachedThreadPool
4.ScheduledThreadPool
https://www.cnblogs.com/jinggod/p/8489169.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY