线程池详解

线程池优点

  • 降低资源消耗:通过复用线程,降低创建和销毁线程的损耗。
  • 提高响应速度:任务不需要等待线程创建就能立即执行。
  • 提高线程的可管理性:使用线程池可以进行统一的分配、调优和监控。

线程池参数

  • corePoolSize:核心线程数,是指线程池中长期存活的线程数(正式工)
  • maxPoolSize:最大线程数,线程池允许创建的最大线程数量,当线程池的任务队列满了之后,可以创建的最大线程数(正式工+临时工)。
  • keepAliveTime:空闲线程存活时间,当线程池中没有任务时,会销毁一些线程,销毁的线程数(辞退临时工)
  • TimeUnit:时间单位。
  • BlockingQueue:线程池任务队列,线程池存放任务的队列,用来存储线程池的所有待执行任务。
  • ThreadFactory:创建线程的工厂,线程池创建线程时调用的工厂方法,通过此方法可以设置线程的优先级、线程命名规则以及线程类型(用户线程还是守护线程)等
  • RejectedExecutionHandler:拒绝策略。

拒绝策略

  • AbortPolicy:新任务被提交后直接被丢弃掉,并抛出异常。
  • DiscardPolicy:新任务被提交后直接被丢弃掉,不抛出异常
  • DiscardOldestPolicy:抛弃队列头部(最旧)的一个任务,并执行当前新任务。
  • CallerRunsPolicy:使用当前调用的线程来执行此任务(谁提交,谁负责)。

执行流程

  • 如果核心线程池没满,就创建新线程来处理任务。
  • 如果核心线程池满了,就将任务加入到阻塞队列。
  • 如果阻塞队列满了,就创建临时线程来处理任务。
  • 如果线程池满了,任务将被拒绝,并按照拒绝策略来处理。

举个例子:

一个线程池,参数corePoolSize = 3,maxPoolSize = 6,BlockingQueue阻塞队列长度为 2 。
有2个任务同时进来,执行execute方法会新建2条线程(核心线程)来执行任务。
又同时进来2个任务(现在总共4个任务),这时候会再新建一个线程来处理任务,由于核心线程池已满(3个),多
出来的任务将会放入阻塞队列中等待空闲线程运行。
又同时进来2个任务(现在总共6个任务),线程池将任务放入阻塞队列中,队列也满了。次数核心线程使用完了,
剩余的1个任务,线程池将会创建一个临时线程(存活时间由keepAliveTime决定)来处理任务。
新线程将使当前运行的线程超出maxPoolSize,任务将被拒绝。执行拒绝策略。        

总结 :任务进入到线程池中,首先生成线程是核心线程(长期存活在线程池中),任务数太多的情况下,会将任务放进阻塞队列中等待空闲线程执行。
在。如果任务在阻塞队列中不够放的时候,会生成临时线程来处理任务(前提是核心线程都在执行任务)。如果线程数达到最大线程,线程池
开始执行拒绝策略

Excutors创建线程池方式

  • FixedThreadPool 

该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。

  • SingleThreadExecutor

方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。

  • CachedThreadPool

该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。

  • ScheduledThreadPoolExecutor

是一个使用线程池执行定时任务的类,相较于Java中提供的另一个执行定时任务的类Timer,其主要有如下两个优点:

使用多线程执行任务,不用担心任务执行时间过长而导致任务相互阻塞的情况,Timer是单线程执行的,因而会出现这个问题;
不用担心任务执行过程中,如果线程失活,其会新建线程执行任务,Timer类的单线程挂掉之后是不会重新创建线程执行后续任务的。

FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE ,可能堆积大量的请求,从而导致 OOM。
CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数,Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。

建议是使用ThreadPoolExecutor创建线程池,更加明确线程池运行规则,避免资源浪费

线程池状态

  •  Running(运行状态)

线程池新建或调用execute()方法后,处于运行状态,能够接收新的任务。

  •  Shutdown(关闭状态)

线程池调用shutdown()方法后,线程池的状态会变为Shutdown。此时线程池不再接收新的任务,但会执行已提交的等待任务队列中的任务。

  • Stop(停止状态)

人为调用shutdownNow()方法后,线程池的状态会变为Stop。此时线程池不再接收新的任务,并且会中断正在处理中的任务。

  • Tidying(整理状态)

当线程池处于Shutdown或Stop状态时,如果等待队列中还有未执行的任务,则线程池将执行整理操作,将等待队列中的未执行任务移除,并保存到一个列表中。

  • Terminated(终止状态)

当线程池处于Shutdown状态,并且等待队列中的任务全部执行完毕,或者在Stop状态下,线程池内部的所有线程都已经终止时,线程池进入Terminated状态。

线程池常用API

  • 提交任务方法

execute(): 执行任务,无返回值;

submit(): 提交任务 task,用返回值 Future 获得任务执行结果;

invokeAll(): 提交所有任务;

invokeAny(): 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消

  • 停止线程池方法

shutdown():线程池状态变为 SHUTDOWN,不会接收新任务,但已提交任务会执行完(在队列中的任务);

shutdownNow():线程池状态变为 STOP,不会接收新任务,会将队列中的任务返回,并用 interrupt 的方式中断正在执行的任务

什么是上下文切换

当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。

线程池如何设置的大小

过小会导致程序不能充分地利用系统资源、容易导致饥饿
过大会导致更多的线程上下文切换,占用更多内存

  • CPU 密集型

对于计算密集型的应用,完全是靠CPU的核数来工作,所以为了让它的优势完全发挥出来,避免过多的线程上下文切换,比较理想方案是:
通常采用 线程数 = CPU核数+1(也可以设置成 CPU核数*2) 能够实现最优的 CPU 利用率,+1 是保证当线程由于页缺失故障(操作系统)或其它原因导致暂停时,额外的这个线程就能顶上去,保证 CPU 时钟周期不被浪费

  •  I/O 密集型

这类任务对CPU的使用率不高,所以可以设置较大点的线程池.例如,当你执行业务计算时,这时候会使用 CPU 资源,但当你执行 I/O 操作时、远程 RPC 调用时,包括进行数据库操作时,这时候 CPU 就闲下来了,线程就会处于等待状态,当IO结束,数据准备好后,线程才会继续执行
对于IO密集型的应用,可以多设置一些线程池中线程的数量,这样就能让在等待IO的这段时间内,线程可以去做其它事,提高并发处理效率

常见的 BlockingQueue

ArrayBlockingQueue(常用)

由数组结构组成的有界阻塞队列。创建 ArrayBlockingQueue 时,我们还 可以控制对象的内部锁是否采用公平锁,默认采用非公平锁。

LinkedBlockingQueue(常用) 

由链表结构组成的有界(但大小默认值为 integer.MAX_VALUE)阻塞队列。

PriorityBlockingQueue

支持优先级排序的无界阻塞队列。

SynchronousQueue

一种无缓冲的等待队列

LinkedTransferQueue
LinkedTransferQueue 是一个由链表结构组成的无界阻塞 TransferQueue 队 列。相对于其他阻塞队列,LinkedTransferQueue 多了 tryTransfer 和 transfer 方法。 LinkedTransferQueue 采用一种预占模式;

LinkedBlockingDeque

由链表组成的双向阻塞队列

特定场景如何选择合适的队列实现

以LinkedBlockingqueue 和 ArrayBlockingQueue 和 SynchronousQueue为例。

考虑应用场景对边界应用的要求。ArrayBlockingQueue 有明确的容量限制,而LinkedBlockingQueue取决于我们是否在创建时指定。SynchronousQueue不能缓存任何数据。
从空间利用率的角度分析。ArrayBlockingQueue要比LinkedBlockingQueue紧凑,因为不需要创建节点。但初始分配阶段就需要一段连续的空间,所以初始内存需求更大。
通用场景中,LinkedBlockingQueue 的吞吐量比 ArrayBlockingQueue 更紧凑,因为它实现了更加细粒度的锁操作。
ArrayBlockingQueue实现比较简单,性能更好预测,属于比较稳健的选手。

ArrayBlockingQueue原理
  • ArrayBlockingQueue是一个有界阻塞队列,初始化时需要指定容量大小。
  • 在生产者-消费者模型中使用时,如果生产速度和消费速度基本匹配的情况下,使用ArrayBlockingQueue是个不错选择;当如果生产速度远远大于消费速度,则会导致队列填满,大量生产线程被阻塞。
  • 使用独占锁ReentrantLock实现线程安全,入队和出队操作使用同一个锁对象,也就是只能有一个线程可以进行入队或者出队操作;这也就意味着生产者和消费者无法并行操作,在高并发场景下会成为性能瓶颈。
LinkedBlockingQueue对比ArrayBlockingQueue

ArrayBlockingQueue中,使用了一个ReentrantLock lock作为入队和出队的锁,并使用两个条件notEmptynotFull来进行线程间通信。而在 LinkedBlockingQueue中,使用了两个锁putLocktakeLock分别作为入队和出队的锁,同样使用了两个锁的两个条件notFullnotEmpty进行线程间通信。由于在ArrayBlockingQueue中,入队和出队操作共用了同一个锁,所以两个操作之间会有相互影响;而在LinkedBlockingQueue中,入队和出队操作分别使用不同的锁,则入队和出队互不影响,可以提供队列的操作性能。

参考:

多线程基础(六、二)BlockingQueue用法详解之ArrayBlockingQueue-CSDN博客

多线程基础(六、三)BlockingQueue用法详解之LinkedBlockingQueue_linkedblockingqueue用法-CSDN博客

  • 26
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值