线程池的饱和策略有哪些?
如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时执行的策略。ThreadPoolExecutor 定义的一些策略:
1、ThreadPoolExecutor.AbortPolicy:抛出 RejectedExecutionException来拒绝新任务的处理。
2、ThreadPoolExecutor.CallerRunsPolicy:直接在调用execute方法的线程(主线程)中运行(run)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。
3、ThreadPoolExecutor.DiscardPolicy:不处理新任务,直接丢弃掉。
4、ThreadPoolExecutor.DiscardOldestPolicy:此策略将丢弃最早的未处理的任务请求。
线程池处理任务的流程了解吗?
上下文切换:通常是计算密集型的。
当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。
Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。
如何设定线程池的大小?设置的线程池数量太小的话,可能会导致大量的请求/任务在任务队列中排队等待执行,甚至会出现任务队列满了之后任务/请求无法处理的情况;设置线程数量太大,大量线程可能会同时在争取 CPU 资源,这样会导致大量的上下文切换,从而增加线程的执行时间,影响了整体执行效率。
简单并且适用面比较广的公式:
CPU 密集型任务(N+1): 一旦有任务暂停(如发生缺页中断等),CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
I/O 密集型任务(2N): 系统会用大部分的时间来处理 I/O 交互,这时就可以将 CPU 交出给其它线程使用。
线程数更严谨的计算的方法应该是:最佳线程数 = N(CPU 核心数)∗(1+WT(线程等待时间)/ST(线程计算时间)),其中 WT(线程等待时间)=线程运行总时间 - ST(线程计算时间)。
如何动态修改线程池的参数?
美团技术团队的思路是主要对线程池的核心参数实现自定义可配置。这三个核心参数是:
1、corePoolSize : 核心线程数线程数定义了最小可以同时运行的线程数量。
2、maximumPoolSize : 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
3、workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。
新任务来先判断当前运行的线程数量是否达到核心线程数,没有的话就执行任务,达到的话就把任务存到队列,当队列中存放的任务达到队列容量的时候,把可以同时运行的线程数量变为最大线程数。如果同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时执行饱和策略。
如何支持参数动态配置? ThreadPoolExecutor 提供了一些参数的设置方法,美团还自定义了一个动态指定队列长度的方法。
如何设计一个能够根据任务的优先级来执行的线程池?
可以考虑使用 PriorityBlockingQueue (优先级无界阻塞队列)作为任务队列。
要想让 PriorityBlockingQueue 实现对任务的排序,传入其中的任务必须是具备排序能力的,方式有两种:
1、提交到线程池的任务实现 Comparable 接口,并重写 compareTo 方法来指定任务之间的优先级比较规则。
2、创建 PriorityBlockingQueue 时传入一个 Comparator 对象来指定任务之间的排序规则(推荐)。
存在的一些问题及解决方案:
1、PriorityBlockingQueue 是无界的,可能堆积大量的请求,从而导致 OOM。(继承PriorityBlockingQueue 并重写一下 offer 方法(入队)的逻辑,当插入的元素数量超过指定值就返回 false 。)
2、可能会导致饥饿问题,即低优先级的任务长时间得不到执行。(可以通过优化设计来解决,比如等待时间过长的任务会被移除并重新添加到队列中,但是优先级会被提升。)
3、由于需要对队列中的元素进行排序操作以及保证线程安全(并发控制采用的是可重入锁 ReentrantLock),因此会降低性能。(由于要对任务进行排序操作。这是没法避免的。并且,对于大部分业务场景来说,这点性能影响是可以接受的。)