线程池的7⼤参数和⼯作原理
线程池的7⼤参数和⼯作原理
线程池到底考什么?
为什么要使⽤线程池?
这⾥借⽤《Java 并发编程的艺术》提到的来说⼀下使⽤线程池的好处:
1. 降低资源消耗。通过重复利⽤已创建的线程降低线程创建和销毁造成的消耗。
2. 提⾼响应速度。当任务到达时,任务可以不需要的等到线程创建就能⽴即执⾏。
3. 提⾼线程的可管理性。线程是稀缺资源,如果⽆限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使⽤线程池可以进⾏统
⼀的分配,调优和监控。
如何去创建⼀个线程池?
《阿⾥巴巴Java开发⼿册》中强制线程池不允许使⽤ Executors 去创建,⽽是通过 ThreadPoolExecutor 的⽅式,这样的处理⽅式让写的同学更加明确线程池的运⾏规则,规避资源耗尽的风险。这⾥的请求队列长度来源于不同线程池选择不同的阻塞队列。
通过Executor 框架的⼯具类Executors来实现我们可以创建三种类型的ThreadPoolExecutor:(第四种时间调度线程池我没了解过)
FixedThreadPool :该⽅法返回⼀个固定线程数量的线程池。该线程池中的线程数量始终不变。当有⼀个新的任务提交时,线程池中若有空闲线程,则⽴即执⾏。若没有,则新的任务会被暂存在⼀个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
SingleThreadExecutor: ⽅法返回⼀个只有⼀个线程的线程池。若多余⼀个任务被提交到该线程池,任务会被保存在⼀个任务队列中,待线程空闲,按先⼊先出的顺序执⾏队列中的任务。
CachedThreadPool: 该⽅法返回⼀个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复⽤,则会优先使⽤可复⽤的线程。若所有线程均在⼯作,⼜有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执⾏完毕后,将返回线程池进⾏复⽤。
既然不能使⽤ Executors 去创建,那么我们可以使⽤通过构造⽅法实现,这也就是相当于我们⼿动配置线程池的参数。
线程池的7⼤参数
2.maximumPoolSize : 当队列中存放的任务达到队列容量的时候,当前可以同时运⾏的线程数量变为最⼤线程数。
**3.keepAliveTime:**当线程池中的线程数量⼤于 corePoolSize 的时候,如果这时没有新的任务提交,核⼼线程外的线程不会⽴即销毁,⽽是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁;
4.unit : keepAliveTime 参数的时间单位。
5.workQueue: 当新任务来的时候会先判断当前运⾏的线程数量是否达到核⼼线程数,如果达到的话,信任就会被存放在队列中。
说到这个⼯作队列,那就要延伸⼀下了。⼯作队列使⽤的其实是阻塞队列,阻塞队列通常可以分为3种:
第⼀种: ArrayBlockingQueue:由数组结构组成的有界阻塞队列
第⼆种: LinkedBlockingQueue:由链表结构组成的有界(但⼤⼩默认值为Integer.MAX_VALUE)阻塞队列。FixedThreadPool和SingleThreadExecutor就使⽤的是LinkedBlockingQueue。
第三种: SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列。CachedThreadPool使⽤的是这种阻塞队列。
6.threadFactory : executor 创建新线程的时候会⽤到,可以理解为线程⼯⼚,该线程来⾃那个线程⼯⼚
**7.handler *饱和策略。关于饱和策略下⾯单独介绍⼀下。
java线程池创建的四种线程池饱和策略
ThreadPoolExecutor 饱和策略定义:
如果当前同时运⾏的线程数量达到最⼤线程数量并且队列也已经被放满了任时,ThreadPoolTaskExecutor 定义⼀些策略:
第⼀种: ThreadPoolExecutor.AbortPolicy:抛出 RejectedExecutionException来拒绝新任务的处理。
第⼆种: ThreadPoolExecutor.CallerRunsPolicy:调⽤者机制,将任务返回给调⽤执⾏⾃⼰的线程运⾏任务。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。
第三种: ThreadPoolExecutor.DiscardPolicy: 不处理新任务,直接丢弃掉。(个⼈认为最靠谱的⽅式)
第四种: ThreadPoolExecutor.DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求。
⾃⼰创建线程池怎样指定线程数?
线程池⽀点线程数⾸先要考虑⾃⼰⾝的业务是什么样的。是CPU密集型还是IO密集型?
假设运⾏应⽤的机器CPU核⼼数是N,那CPU密集型可以先给到N+1,IO密集型可以给2N。
线程池的⼯作原理
为了搞懂线程池的原理,我们需要⾸先分析⼀下 execute⽅法。
通过下图可以更好的对上⾯这 3 步做⼀个展⽰:
⼏个常见的对⽐
Runnable vs Callable
Runnable⾃ Java 1.0 以来⼀直存在,但Callable仅在 Java 1.5 中引⼊,⽬的就是为了来处理Runnable不⽀持的⽤例。Runnable 接⼝不会返回结果或抛出检查异常,但是Callable 接⼝可以。所以,如果任务不需要返回结果或抛出异常推荐使⽤ Runnable 接⼝,这样代码看起来会更加简洁。
execute() vs submit()
execute()⽅法⽤于提交不需要返回值的任务,所以⽆法判断任务是否被线程池执⾏成功与否;
submit()⽅法⽤于提交需要返回值的任务。线程池会返回⼀个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执⾏成功 ,并且可以通过 Future 的 get()⽅法来获取返回值,get()⽅法会阻塞当前线程直到任务完成,⽽使⽤ get(long timeout,TimeUnit unit)⽅法则会阻塞当前线程⼀段时间后⽴即返回,这时候有可能任务没有执⾏完。
⼏种常见的线程池
FixedThreadPool
FixedThreadPool 被称为可重⽤固定线程数的线程池。通过 Executors 类中的相关源代码来看⼀下相关实现:
从上⾯源代码可以看出新创建的 FixedThreadPool 的 corePoolSize 和 maximumPoolSize 都被设置为
nThreads,这个 nThreads 参数是我们使⽤的时候⾃⼰传递的。
执⾏任务过程介绍
上图说明:
如果当前运⾏的线程数⼩于 corePoolSize, 如果再来新任务的话,就创建新的线程来执⾏任务;
当前运⾏的线程数等于 corePoolSize 后, 如果再来新任务的话,会将任务加⼊ LinkedBlockingQueue;
线程池中的线程执⾏完 ⼿头的任务后,会在循环中反复从 LinkedBlockingQueue 中获取任务来执⾏;
为什么不推荐使⽤FixedThreadPool?
FixedThreadPool 使⽤⽆界队列 LinkedBlockingQueue(队列的容量为 Intger.MAX_VALUE)作为线程池的⼯作队列会对线程池带来如下影响 :
由于使⽤⽆界队列时 maximumPoolSize 将是⼀个⽆效参数,因为不可能存在任务队列满的情况。所以,通过创建 FixedThreadPool的源码可以看出创建的 FixedThreadPool 的 corePoolSize 和 maximumPoolSize 被设置为同⼀个值。
运⾏中的 FixedThreadPool(未执⾏ shutdown()或 shutdownNow())不会拒绝任务,在任务⽐较多的时候会导致 OOM(内存溢出)。
SingleThreadExecutor
SingleThreadExecutor 是只有⼀个线程的线程池。下⾯看看SingleThreadExecutor 的实现:
为什么不推荐使⽤SingleThreadExecutor?
SingleThreadExecutor 使⽤⽆界队列 LinkedBlockingQueue 作为线程池的⼯作队列(队列的容量为 Intger.MAX_VALUE)。SingleThreadExecutor 使⽤⽆界队列作为线程池的⼯作队列会对线程池带来的影响与 FixedThreadPool 相同。说简单点就是可能会导致OOM。
CachedThreadPool 详解
CachedThreadPool 是⼀个会根据需要创建新线程的线程池。下⾯通过源码来看看 CachedThreadPool 的实现:
CachedThreadPool 的corePoolSize 被设置为空(0),maximumPoolSize被设置为 Integer.MAX.VALUE,即它是⽆界的,这也就意味着如果主线程提交任务的速度⾼于 maximumPool 中线程处理任务的速度时,CachedThreadPool 会不断创建新的线程。极端情况下,这样会导致耗尽 cpu 和内存资源。
为什么不推荐使⽤CachedThreadPool?
CachedThreadPool允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建⼤量线程,从⽽导致 OOM。
线程池⼤⼩确定
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 操作完成上。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论