线程池

什么是线程池?

线程池是指在初始化一个多线程应用程序过程中创建一个线程集合,然后在需要执行新的任务时重用这些线程而不是新建一个线程。线程池中线程的数量通常完全取决于可用内存数量和应用程序的需求。然而,增加可用线程数量是可能的。线程池中的每个线程都有被分配一个任务,一旦任务已经完成,线程回到池中并等待下一次分配任务。

简单来说: 这是一个“池化”的概念,线程池是指在初始化的时候创建一个线程集合。当需要执行的任务时,由池中的线程去执行任务,完成任务后回到线程池中以便下次复用。

使用线程池的好处

  • 降低资源消耗,通过复用已有线程降低线程的频繁创建与销毁。
  • 提高响应效率,当需要执行新任务时无需等线程创建就能执行。
  • 提高线程可管理性,使用线程池进行统一的分配及调优监控。

线程的创建方式

线程的创建方式

  • 继承Thread类
  • 实现Runnable接口
  • 实现Callable接口,再使用FutureTask类包装Callable创建。

Callable方式和其他方式创建线程的区别

  • Callable核心方法是call() , Runnable是run()
  • Callable方式执行完可获得返回值,而Runnable没有返回值
  • Callable方式的call()可抛异常 , Runnable方式的run()不可以

线程池的创建方式

JDK中Executors类为我们提供了不同方式创建线程池的方法来创建线程池(executor service);无论是Runnable还是Callable的实现类都可以被ThreadPoolExecutorScheduledThreadPoolExecutor执行。

通过Executors提供四种线程池分别是:

  • newCachedThreadPool (常用) 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
  • newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
  • newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
  • newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
  • newWorkStealingPool是新的线程池类ForkJoinPool的扩展。

线程池参数及原理

ThreadPoolExecutor

  • corePoolSize: 核心线程数
  • maximumPoolSize: 最大线程数
  • workQueue: 工作队列
  • keepAliveTime:空闲线程存活时间
  • RejectedExecutionHandler: 拒绝策略
  • ThreadFactory:创建线程的工厂
  • TimeUnit: keepAliveTime的时间单位

原理:

  • corePoolSize: 核心线程数量 ,当有新任务在execute()方法提交时,会执行以下判断:
  1. 如果运行的线程少于corePoolSize, 则创建新线程来处理任务,即使线程池中的其他线程是空闲的。

  2. 如果线程池中的线程数量大于等于corePoolSize且小于maximumPoolSize,则只有当workQueue满时才创建新的线程去处理任务;

  3. 如果设置的corePoolSizemaximumPoolSize相同,则创建的线程池的大小是固定的,这时如果有新任务提交,若workQueue未满, 则将请求放入workQueue中,等待有空闲的线程去从workQueue中取任务并处理;

  4. 如果运行的线程数量大于等于maximumPoolSize,这时如果workQueue已经满了,则通过handler所指定的策略来处理任务;

  5. 所以,任务提交时,判断的顺序为corePoolSsize --> workQueue --> maximumPoolSize

  • maximumPoolSize: 最大线程数量;

  • workQueue: 等待队列,当任务提交时,如果线程池中的线程数量大于等于corePoolSize的时候,把该任务封装成一一个Worker对象放入等待队列;

  • workQueue: 保存等待执行的任务的阻塞队列,当提交一个新的任 务到线程池以后,线程池会根据当前线程池中正在运行着的线程的数量来决定对该任务的处理方式,主要有以下几种处理方式:

  1. 直接切换:这种方式常用的队列是SynchronousQueue,但现在还没有研究过该队列,这里暂时还没法介绍;

  2. 使用无界队列: 一般使用基于链表的阻塞队列inkedBlockingQueue。如果使用这种方式,那么线程池中能够创建的最大线程数就是corePoolSize,而maximumPoolSize就不会起作用了(后面也会说到)。当线程池中所有的核心线程都是RUNNING状态时,这时一个新的任务提交就会放入等待队列中。

  3. 使用有界队列:一般使用ArrayBlockingQueue. 使用该方式可以将线程池的最大线程数量限制为maximumPoolSize,这样能够降低资源的消耗,但同时这种方式也使得线程池对线程的调度变得更困难,因为线程池和队列的容量都是有限的值,所以要想使线程池处理任务的吞吐率达到一个相对合理的范围,又想使线程调度相对简单,并且还要尽可能的降低线程池对资源的消耗,就需要合理的设置这两个数量。

  4. keepAlive Time:线程池维护线程所允许的空闲时间。当线程池中的线程数量大于corePoolSize的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime;

  5. threadFactory: 它是ThreadFactory类型的变量,用来创建新线程。默认使用Executors.defaultThreadFactory() 来创建线程。使用默认的ThreadFactory来创建线程时,会使新创建的线程具有相同的NORM_ PRIORITY优先级并且是非守护线程,同时也设置了线程的名称。

  6. handler:它是RejectedExecutionHandler类型的变量,表示线程池的饱和策略。如果阻塞队列满了并且没有空闲的线程,这时如果继续提交任务,就需要采取一种策略处理该任务。线程池提供了4种策略:

  • AbortPolicy: 直接抛出异常,这是默认策略;
  • CallerRunsPolicy: 用调用者所在线程来执行任务;
  • DiscardOldestPolicy: 丢弃阻塞队列中靠最前的任务,并执行当前任务;
  • DiscardPolicy: 直接丢弃任务;

线程池中的线程初始化

默认情况下,创建线程池之后,线程池中是没有线程的,需要提交任务之后才会创建线程。在实 际中如果需要线程池创建之后立即创建线程,可以通过以下两个方法办到:

  • prestartCoreThread(): 初始化一个核心线程;
  • prestartAllCoreThreads(): 初始化所有核心线程

线程池的关闭

ThreadPoolExecutor提供了两个方法,用于线程池的关闭

  • shutdown(): 不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止, 但再也不会接受新的任务
  • shutdownNow(): 立即终止线程池,井尝试打断正在执行的任务,并且清空任务缓存队列,返 回尚未执行的任务

异常处理

  • execute: Apache Commons提供的BasicThreadFactoryBuilder
  • submit: get try+catch

任务拒绝策略

当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize, 如果还有任务 到来就会采取任务拒绝策略,通常有以下四种策略:

  • ThreadPoolExecutor .AbortPolicy:丢弃任务并抛出RejectedExecutionException异常.
  • ThreadPoolExecutor.DiscardPolicy: 也是丢弃任务,但不抛出异常。
  • hreadPoolExecutor .DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
  • ThreadPoolExecutor.CallerRunsPolicy: 由调用线程处理该任务

线程池大小

粗略:

  • 如果是CPU密集型任务,就需要尽量压榨CPU,参考值可以设为NCPU+1
  • 如果是IO密集型任务,参考值可以设置为2*NCPU

精确: ((线程等待时间+线程CPU时间) /线程CPU时间)*CPU数目

最佳:压测

任务缓存队列

在前面我们多次提到了任务缓存队列,即workQueue, 它用来存放等待执行的任务。
BlockingQueue是个接口,你需要使用它的实现;之一来使用BlockingQueue, java.util.concurrent 包下具有以下BlockingQueue接口的实现类:

  • ArrayBlockingQueue: ArrayBlockingQueue 是一个有界的阻塞队列,其内部实现是将对象放 到一个数组里。有界也就意味着,它不能够存储无限多数量的元素。它有一个同一时间能够存 储元素数量的上限。你可以在对其初始化的时候设定这个上限,但之后就无法对这个上限进行 修改了(译者注:因为它是基于数组实现的,也就具有数组的特性: 一旦初始化,大小就无法修改)。

  • LinkedBlockingQueue: LinkedBlockingQueue 内部以一个链式结构(链接节点)对其元素进行 存储。如果需要的话,这一链式结构可以选择一个上限。如果没有定义上限,将使用 Integer.MAX_ VALUE作为上限。

  • DelayQueue: DelayQueue对元素进行持有直到一个特定的延迟到期。注入其中的元素必须实 现java.util.concurrent.Delayed接口.

  • PriorityBlockingQueue: PriorityBlockingQueue 是一一个无界的并发队列。它使用了和类java.util.PriorityQueue 一样的排序规则。你无法向这个队列中插入null 值。所有插入到 PriorityBlockingQueue的元素必须实现java. lang.Comparable接口。因此该队列中元素的 排序就取决于你自己的Comparable实现。

  • SynchronousQueue: SynchronousQueue 是一个特殊的队列,它的内部同时只能够容纳单个 元素。如果该队列已有元素的话,试图向队列中插入一个新元素的线程将会阻塞,直到另一 个线程将该元素从队列中抽走。同样,如果该队列为空,试图向队列中抽取一个元素的线程将会阻塞,直到另一个线程向队列中插入了一条新的元素。据此,把这个类称作一个队列显然是 夸大其词了。它更多像是一个汇合点。

线程池总结

1.线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。

2.当调用execute()方法添加一个任务时,线程池会做如下判断:

  • 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
  • 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;

3.如果这时候队列满了,而且正在运行的线程数量小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;

4.如果队列满了,而且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会抛出异常RejectExecutionException.

5.当一个线程完成任务时,它会从队列中取下一个任务来执行。

6.当一个线程无事可做,超过一定的时间(keepAliveTime) 时,线程池会判断,如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值