java并发-线程池

线程池提出的思想基础

  • 降低资源的消耗。通过重复利用已经创建的线程降低线程创建和销毁的消耗
  • 提高响应速度。当任务到达的时候,任务不需要等待线程创建直接可以执行
  • 提高对线程的可管理性。线程是稀缺资源,如果无线的创建,不仅会消耗资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

线程池的实现原理

线程池框架的结构(主要由三部分组成)
任务(Runnable / Callable)
  • 我们将我们的结果封装到Runnable或是Callable中,交给ThreadPoolExecutor或ScheduledThreadPoolExecutor中去执行
执行(Executor)

在这里插入图片描述

  • 我们的任务交给ThreadPoolExecutor或ScheduleThreadPoolExecutor类中的executor或submit去执行
异步结果返回 (future)
  • future接口以及future接口的实现类futureTask类都可以代表异步计算的结果
  • 我们的submit()方法在执行callable任务时会返回一个futurtask对象来异步返回结果(准确的说应该是同步非阻塞的,需要我们去主动获取返回结果)
Executor框架的使用示意图

在这里插入图片描述

  • 主线程首先要创建实现 Runnable 或者 Callable 接口的任务对象。
  • 把创建完成的实现 Runnable/Callable接口的 对象直接交给 ExecutorService 执行: ExecutorService.execute(Runnable command))或者也可以把 Runnable 对象或Callable 对象提交给 ExecutorService 执行(ExecutorService.submit(Runnable task)或 ExecutorService.submit(Callable task))。
  • 如果执行 ExecutorService.submit(…),ExecutorService 将返回一个实现Future接口的对象(我们刚刚也提到过了执行 execute()方法和 submit()方法的区别,submit()会返回一个 FutureTask 对象)。由于 FutureTask 实现了 Runnable,我们也可以创建 FutureTask,然后直接交给 ExecutorService 执行。
  • 最后,主线程可以执行 FutureTask.get()方法来等待任务执行完成。主线程也可以执行 FutureTask.cancel(boolean mayInterruptIfRunning)来取消此任务的执行。
线程框架的核心类ThreadPoolExcutor的3,7,4总结,还有线程池的状态
三大种类
  • FixedThreadPool:可重用固定线程数的线程池(核心线程数=最大线程数),不推荐使用(主要原因是它的阻塞队列LinkedBlockingQueue使用的是无边界的队列,最大值是Interger的最大值,这是一个无效的参数

  • 在这里插入图片描述

  • singleThreadExecutor:核心线程数=最大线程数 = 1 - 在这里插入图片描述

  • ChachedThreadPool: 是一个会根据需要创建新线程的线程池(核心线程为0,最大线程数为Integer.MAXVALUE)

  • 在这里插入图片描述

七大参数(关乎线程池的运行的原理)
  • corePoolSize : 核心线程数线程数定义了最小可以同时运行的线程数量。
  • maximumPoolSize : 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数
  • keepAliveTime:当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁;(简言之非核心线程的空闲线程的存活时间)
  • unit : keepAliveTime 参数的时间单位
  • workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。(阻塞对列
  • threadFactory :executor 创建新线程的时候会用到。(线程工厂
  • handler :饱和策略。关于饱和策略下面单独介绍一下。
    在这里插入图片描述
    在这里插入图片描述
四种饱和策略
  • ThreadPoolExecutor.AbortPolicy :抛出 RejectedExecutionException来拒绝新任务的处理。(抛异常直接拒绝
  • ThreadPoolExecutor.CallerRunsPolicy :调用执行自己的线程运行任务,也就是直接在调用execute方法的线程中运行(run)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。(将任务交给运行线程池的那个线程去执行
  • ThreadPoolExecutor.DiscardPolicy :不处理新任务,直接丢弃掉。(直接抛弃不处理
  • ThreadPoolExecutor.DiscardOldestPolicy : 此策略将丢弃最早的未处理的任务请求。(抛弃未处理任务中最早的
线程池的状态

在这里插入图片描述

  • RUNNING:状态说明,线程池处在RUNNING状态时,能够接收新任务,以及对已添加的任务进行处理;状态切换,线程池的初始化状态是RUNNING。换句话说,线程池被一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0
  • SHUTDOWN: 状态说明,线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务;状态切换,调用线程池的shutdown()接口时,线程池由RUNNING -> SHUTDOWN。
  • STOP:状态说明,线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务;状态切换,调用线程池的shutdownNow()接口时,线程池由(RUNNING or SHUTDOWN ) -> STOP。
  • TIDYING:状态说明,当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现; 状态切换,当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING,当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。
  • TERMINATED:状态说明,线程池彻底终止,就变成TERMINATED状态;状态切换,线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED。

线程池的注意事项

为什么不推荐使用FIxedThreadPool?

FixedThreadPool 使用无界队列 LinkedBlockingQueue(队列的容量为 Integer.MAX_VALUE)作为线程池的工作队列会对线程池带来如下影响

  • 当线程池中的线程数达到 corePoolSize 后,新任务将在无界队列中等待,因此线程池中的线程数不会超过 corePoolSize;
  • 由于使用无界队列时 maximumPoolSize 将是一个无效参数,因为不可能存在任务队列满的情况。所以,通过创建 FixedThreadPool的源码可以看出创建的 FixedThreadPool 的 corePoolSize 和 maximumPoolSize 被设置为同一个值。
  • 由于 1 和 2,使用无界队列时 keepAliveTime 将是一个无效参数;
  • 运行中的 FixedThreadPool(未执行 shutdown()或 shutdownNow())不会拒绝任务,在任务比较多的时候会导致 OOM(内存溢出)
为什么也不推荐使用SingleThreadExecutor?
  • SingleThreadExecutor 使用无界队列 LinkedBlockingQueue 作为线程池的工作队列(队列的容量为 Intger.MAX_VALUE)。SingleThreadExecutor 使用无界队列作为线程池的工作队列会对线程池带来的影响与 FixedThreadPool 相同。说简单点就是可能会导致 OOM,
为什么不推荐使用CachedThreadPool?
  • CachedThreadPool允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM
ScheduledThreadPoolExecutor的执行任务步骤?

在这里插入图片描述

线程池大小确定

在线程中的主要成本就是线程的上下文切换,因为涉及到操作系统,会涉及状态切换,有用户态切换到内核态,消耗就比较大

  • 如果我们设置的线程池数量太小的话,如果同一时间有大量任务/请求需要处理,可能会导致大量的请求/任务在任务队列中排队等待执行,甚至会出现任务队列满了之后任务/请求无法处理的情况,或者大量任务堆积在任务队列导致 OOM。这样很明显是有问题的! CPU 根本没有得到充分利用
  • 如果我们设置线程数量太大,大量线程可能会同时在争取 CPU 资源,这样会导致大量的上下文切换,从而增加线程的执行时间,影响了整体执行效率。
进行分类CPU密集型和IO密集型
  • CPU 密集型任务(N+1)这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
  • I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。(因为大量时间用在处理IO,可以多配制线程在IO等待期间去执行其他任务
  • 怎么判断是IO密集型还是CPU密集西?CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序。单凡涉及到网络读取,文件读取这类都是 IO 密集型,这类任务的特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上。
给不同类别的任务使用不同的线程池?
案例因为线程池中,出现了循环依赖,导致的死锁问题。本案例来自:《线程池运用不当的一次线上事故》
  • 在这里插入图片描述
  • 因为在任务中使用了Executors创建了固定大小的线程池,但是其中fixedThreadPool的缺点在于,阻塞对列无边界,导致死锁产生时,大量的任务阻塞在了对列中,无法使用饱和策略,最后导致OOM。
  • 这里产生死锁的原因在于,父子任务都在同一个线程池中,当父任务的执行需要子任务执行完,如果出现,父任务在线程池中执行,子任务却在阻塞对列中等待执行,此时线程池饱和,无法加入新的任务,就出现了死锁。
  • 在这里插入图片描述
  • 在这里插入图片描述
  • 解决办法:就是新增一个用于执行子任务的线程池,将父子任务分隔开。
实现线程池的自定义配置的思路和方法
  • 线程池中影响其最重要的参数:corePoolSize,maximunPoolSize,workQueue
  • 实现这三个参数的自定义配置,就可以实现线程池的自定义配置
  • 对于corePoolSize,我们可以调用serCorePoolSize()方法,来动态的改变其值:调用这个方法的时候会判断,当前的工作线程数是否大于corePoolSize,如果大于就回收
  • 对于阻塞对列workQueue可以使用ResizableCapacityLinkedBlockIngQueue的对垒这个对列的长度可伸缩,(主要的是把其中capacity的final去掉了,将其变为可变的了)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值