栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

Java 异步任务执行服务 线程池

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Java 异步任务执行服务 线程池

这里写目录标题
    • 18.2 线程池
      • 18.2.1 理解线程池
        • 1.线程池大小
        • 2.队列
        • 3.任务拒绝策略
        • 4.线程工厂
        • 5.关于核心线程的特殊配置
      • 18.2.2 工厂类Executors
      • 18.2.3 线程池的死锁
      • 18.2.4 小结
    • 参考目录

18.2 线程池

      线程池是并发程序中一个非常重要的概念和技术。 线程池,顾名思义,就是一个线程的池子,里面有若干线程,它们的目的就是执行提 交给线程池的任务,执行完一个任务后不会退出,而是继续等待或执行 新任务。线程池主要由两个概念组成:一个是任务队列; 另一个是工作者线程 。工作者线程主体就是一个循环,循环从队列中接受任务并执行,任务队列保存待执行的任务。

      线程池的概念类似于生活中的一些排队场景,比如在医院排队挂号、在银行排队办理业务等,一般都由若干窗口提供服务,这些服务窗口类似于工作者线程;队列的概念是类似的,只是在现实场景中,每个窗口经常有一个单独的队列,这种排队难以公平,随着信息化的发展, 越来越多的排队场合使用虚拟的统一队列,一般都是先拿一个排队号, 然后按号依次服务。

      线程池的优点是显而易见的:

      ·它可以重用线程,避免线程创建的开销。

      ·任务过多时,通过排队避免创建过多线程,减少系统资源消耗和竞争,确保任务有序完成。

      Java并发包中线程池的实现类是ThreadPoolExecutor,它继承自 AbstractExecutor-Service,实现了ExecutorService,基本用法与上节介绍 的类似,我们就不赘述了。不过,ThreadPoolExecutor有一些重要的参数,理解这些参数对于合理使用线程池非常重要,接下来,我们探讨这 些参数。

18.2.1 理解线程池

      先来看ThreadPoolExecutor的构造方法。ThreadPoolExecutor有多个构造方法,都需要一些参数,主要构造方法有:

      第二个构造方法多了两个参数threadFactory和handler,这两个参数 一般不需要,第一个构造方法会设置默认值。参数corePoolSize、 maximumPoolSize、keepAliveTime、unit用于控制线程池中线程的个数,workQueue表示任务队列,threadFactory用于对创建的线程进行一 些配置,handler表示任务拒绝策略。下面我们详细探讨下这些参数。

1.线程池大小

      线程池的大小主要与4个参数有关:

      ·corePoolSize:核心线程个数。

      ·maximumPoolSize:最大线程个数。

      ·keepAliveTime和unit:空闲线程存活时间。

      maximumPoolSize表示线程池中的最多线程数,线程的个数会动态变化,但这是最大值,不管有多少任务,都不会创建比这个值大的线程个数。corePoolSize表示线程池中的核心线程个数,不过,并不是一开 始就创建这么多线程,刚创建一个线程池后,实际上并不会创建任何线程。

      一般情况下,有新任务到来的时候,如果当前线程个数小于 corePoolSiz,就会创建一个新线程来执行该任务,需要说明的是,即使 其他线程现在也是空闲的,也会创建新线程。不过,如果线程个数大于 等于corePoolSiz,那就不会立即创建新线程了,它会先尝试排队,需要 强调的是,它是“尝试”排队,而不是“阻塞等待”入队,如果队列满了或 其他原因不能立即入队,它就不会排队,而是检查线程个数是否达到了 maximumPoolSize,如果没有,就会继续创建线程,直到线程数达到 maximumPoolSize。

      keepAliveTime的目的是为了释放多余的线程资源,它表示,当线 程池中的线程个数大于corePoolSize时额外空闲线程的存活时间。也就是说,一个非核心线程,在空闲等待新任务时,会有一个最长等待时间,即keepAliveTime,如果到了时间还是没有新任务,就会被终止。 如果该值为0,则表示所有线程都不会超时终止。

      这几个参数除了可以在构造方法中进行指定外,还可以通过 getter/setter方法进行查看和修改。

      除了这些静态参数,ThreadPoolExecutor还可以查看关于线程和任务数的一些动态数字:

2.队列

      ThreadPoolExecutor要求的队列类型是阻塞队列BlockingQueue,我 们在17.4节介绍过多种BlockingQueue,它们都可以用作线程池的队列, 比如:

      ·linkedBlockingQueue:基于链表的阻塞队列,可以指定最大长 度,但默认是无界的。

      ·ArrayBlockingQueue:基于数组的有界阻塞队列。

      ·PriorityBlockingQueue:基于堆的无界阻塞优先级队列。

      ·SynchronousQueue:没有实际存储空间的同步阻塞队列。

      如果用的是无界队列,需要强调的是,线程个数最多只能达到 corePoolSize,到达core-PoolSize后,新的任务总会排队,参数 maximumPoolSize也就没有意义了。

      对于SynchronousQueue,我们知道,它没有实际存储元素的空间, 当尝试排队时,只有正好有空闲线程在等待接受任务时,才会入队成功,否则,总是会创建新线程,直到达到maximumPoolSize。

3.任务拒绝策略

      如果队列有界,且maximumPoolSize有限,则当队列排满,线程个数也达到了maxi-mumPoolSize,这时,新任务来了,如何处理呢?此 时,会触发线程池的任务拒绝策略。

      默认情况下,提交任务的方法(如execute/submit/invokeAll等)会 抛出异常,类型为RejectedExecutionException。

      不过,拒绝策略是可以自定义的,ThreadPoolExecutor实现了4种处 理方式。

      1)ThreadPoolExecutor.AbortPolicy:这就是默认的方式,抛出异常。

      2)ThreadPoolExecutor.DiscardPolicy:静默处理,忽略新任务,不抛出异常,也不执行。

      3)ThreadPoolExecutor.DiscardOldestPolicy:将等待时间最长的任务扔掉,然后自己排队。

      4)ThreadPoolExecutor.CallerRunsPolicy:在任务提交者线程中执行任务,而不是交给线程池中的线程执行。

      它们都是ThreadPoolExecutor的public静态内部类,都实现了 RejectedExecutionHandler接口,这个接口的定义为:


      当线程池不能接受任务时,调用其拒绝策略的rejectedExecution方法。

      拒绝策略可以在构造方法中进行指定,也可以通过如下方法进行指 定:

      默认的RejectedExecutionHandler是一个AbortPolicy实例,如下所 示:

      而AbortPolicy的rejectedExecution实现就是抛出异常,如下所示:

      我们需要强调下,拒绝策略只有在队列有界,且maximumPoolSize 有限的情况下才会触发。如果队列无界,服务不了的任务总是会排队, 但这不一定是期望的结果,因为请求处理队列可能会消耗非常大的内存,甚至引发内存不够的异常。如果队列有界但maxi-mumPoolSize无限,可能会创建过多的线程,占满CPU和内存,使得任何任务都难以完 成。所以,在任务量非常大的场景中,让拒绝策略有机会执行是保证系统稳定运行很重要的方面。

4.线程工厂

      线程池还可以接受一个参数:ThreadFactory。它是一个接口,定义为:

      这个接口根据Runnable创建一个Thread,ThreadPoolExecutor的默认实现是Executors类中的静态内部类DefaultThreadFactory,主要就是创建 一个线程,给线程设置一个名称,设置daemon属性为false,设置线程优先级为标准默认优先级,线程名称的格式为:pool-<线程池编号>- thread-<线程编号>。如果需要自定义一些线程的属性,比如名称,可以实现自定义的ThreadFactory。

5.关于核心线程的特殊配置

      线程个数小于等于corePoolSize时,我们称这些线程为核心线程, 默认情况下。

      ·核心线程不会预先创建,只有当有任务时才会创建。

      ·核心线程不会因为空闲而被终止,keepAliveTime参数不适用于它。

      不过,ThreadPoolExecutor有如下方法,可以改变这个默认行为。

18.2.2 工厂类Executors

      类Executors提供了一些静态工厂方法,可以方便地创建一些预配置的线程池,主要方法有:

      newSingleThreadExecutor基本相当于调用:

      只使用一个线程,使用无界队列linkedBlockingQueue,线程创建后不会超时终止,该线程顺序执行所有任务。该线程池适用于需要确保所有任务被顺序执行的场合。

      newFixedThreadPool的代码为:

      使用固定数目的n个线程,使用无界队列linkedBlockingQueue,线程创建后不会超时终止。和newSingleThreadExecutor一样,由于是无界队列,如果排队任务过多,可能会消耗过多的内存。

      newCachedThreadPool的代码为:


      它的corePoolSize为0,maximumPoolSize为Integer.MAX_VALUE, keepAliveTime是60秒,队列为SynchronousQueue。它的含义是:当新任务到来时,如果正好有空闲线程在等待任务,则其中一个空闲线程接受该任务,否则就总是创建一个新线程,创建的总线程个数不受限制,对任一空闲线程,如果60秒内没有新任务,就终止。

      实际中,应该使用newFixedThreadPool还是newCachedThreadPool 呢?

      在系统负载很高的情况下,newFixedThreadPool可以通过队列对新任务排队,保证有足够的资源处理实际的任务,而 newCachedThreadPool会为每个任务创建一个线程,导致创建过多的线 程竞争CPU和内存资源,使得任何实际任务都难以完成,这时, newFixedThreadPool更为适用。

      不过,如果系统负载不太高,单个任务的执行时间也比较短, newCachedThreadPool的效率可能更高,因为任务可以不经排队,直接交给某一个空闲线程。

      在系统负载可能极高的情况下,两者都不是好的选择, newFixedThreadPool的问题是队列过长,而newCachedThreadPool的问题是线程过多,这时,应根据具体情况自定义ThreadPoolExecutor,传递合适的参数。

18.2.3 线程池的死锁

      关于提交给线程池的任务,我们需要注意一种情况,就是任务之间有依赖,这种情况可能会出现死锁。比如任务A,在它的执行过程中, 它给同样的任务执行服务提交了一个任务B,但需要等待任务B结束。

      如果任务A是提交给了一个单线程线程池,一定会出现死锁,A在等待B的结果,而B在队列中等待被调度。如果是提交给了一个限定线 程个数的线程池,也有可能因线程数限制出现死锁。

      怎么解决这种问题呢?可以使用newCachedThreadPool创建线程池,让线程数不受限制。另一个解决方法是使用SynchronousQueue,它 可以避免死锁,怎么做到的呢?对于普通队列,入队只是把任务放到了 队列中,而对于SynchronousQueue来说,入队成功就意味着已有线程接受处理,如果入队失败,可以创建更多线程直到maximumPoolSize,如 果达到了maximumPoolSize,会触发拒绝机制,不管怎么样,都不会死锁。

18.2.4 小结

      本节介绍了线程池的基本概念,详细探讨了其主要参数的含义,理解这些参数对于合理使用线程池是非常重要的,对于相互依赖的任务, 需要注意避免出现死锁。

      ThreadPoolExecutor实现了生产者/消费者模式,工作者线程就是消 费者,任务提交者就是生产者,线程池自己维护任务队列。当我们碰到类似生产者/消费者问题时,应该优先考虑直接使用线程池,而非“重新 发明轮子”,应自己管理和维护消费者线程及任务队列。

参考目录

绝大多数内容来自于:Java编程的逻辑 作者: 马俊昌(第18章 异步任务执行服务 18.2 线程池)

Java官方文档
https://docs.oracle.com/javase/specs/index.html

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/424186.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号