JAVA线程池使用简介

JAVA线程池使用简介

线程:

  1. 线程生命周期的开销非常高,
  2. 活跃的线程会消耗系统资源,尤其是内存。如果可运行的线程数量多于可用处理器的数量,那么有些线程将闲置,切闲置的线程同样会占用内存,大量的线程在竞争状态下

增加适当的线程可以提高系统的吞吐率,但如果超出这个范围,再创建更多的线程只能降低程序的执行速度。

任务是一组逻辑工作单元,而线程是使任务异步执行的机制。Java.util.concurrent提供了一种提供了一种灵活的线程池实现作为Executor框架的一部分。在java的类库中,任务执行的主要抽象不是Thread,而是Executor。

      Executor支持多种不同类型的任务执行策略。它提供了一种标准的方法将任务的提交过程与执行过程解耦,并用Runnable来表示任务。Executor的实现还提供了对生命周期的支持,以及统计信息收集、应用程序管理机制和性能监视等机制。

      Executor基于生产者-消费者模式,提交任务-生产者,执行任务-消费者。

线程池:

      与工作队列密切相关,在工作队列中保留了所有等待执行的任务。

      通过调用Executors 中的静态工程方法来创建线程池:

  1. newFixedThreadPool:创建一个固定长度的线程池;
  2. newCacheThreadPool:创建一个可以缓存的线程池,线程池的规模不存在任何限制
  3. newScheduedThreadPool:创建一个固定长度的线程池,并且以延迟或者定时的方式来执行任务(优于Timer);
  4. newSingleThreadExecutor:创建一个单线程的Executor,能确保依照任务在队列中的顺序来执行。

 

Executor的生命周期:

      扩展接口:ExecutorService;

      生命周期:运行、正在关闭、已终止

      void shutdown():平缓关闭,不在接受新的任务,同时等待已提交的任务执行完成—包括在队列里面暂时还未执行的任务。

        List<Runnable> shutdowsNow(): 立即关闭,它将尝试取消所有运行中的任务,并且不在启动任务队列中尚未开始执行的任务,他会返回那些未执行的任务.它试图终止线程是通过Thread.interrupt()方法来实现的,由于该方法作用有限,所以它也可能必须等待正在执行的任务结束后才能完成退出。

 

延迟任务和周期任务

      Timer类负责管理延迟任务和周期任务,但Timer类存在一些缺陷,需要考虑用SecheduledThreadPoolExecutor来代替。

        Timer类在执行任务的时候只会创建一个线程,如果某个线程的执行时间过长,则会破坏其他TimerTask的定时精确性。

      Timer不捕获异常,如果TimerTask抛出未检查的异常(RuntimeException)定时线程将会被终止,正在执行的任务和以后的任务均不会在执行。该问题被称为线程泄漏。

     

Callable和Future


      Runnable的run()方法虽然也能写入到日志文件或者将结果放入某个共享的数据结构中,但是它不能返回一个值,或者抛出一个受检查的异常。

      Callable是一种更好的任务抽象,它认为call()方法将返回一个值,并可能抛出一个异常,在Executor中提供了很多辅助方法将其他类型的任务封装成为Callable,在Executor框架中,已经提交但是尚未执行的任务可以直接取消,但对于已经开始执行的任务只有任务能响应中断的时候才能取消。

      Executor中执行任务有四个生命周期:创建、提交、开始和完成,Future则用来表示一个任务的生命周期,并且提供了相应的方法来判断是否完成或者取消,以及获取任务的结果和取消任务等。ExecutorService中所有的submit方法均会返回一个Future,所以可以将一个Runable或者Callable 提交给一个Executor得到一个Future用来获得任务的执行结果或者取消任务等。同时还可以显示的指定一个Runnable或Callable实例化一个FutureTask。

      如果向Executor提交了一组任务,那么可以通过CompletionService来获取这一组任务的结果。CompletionService是将Executor和BlackingQueue结合,ExecutorCompletionService是CompletionService的实现类,在其构造函数中创建一个BlackingQueue保存计算完成的结果即可。可以将多个任务通过invokeAll提交到一个ExecutorService中,返回值为一组Future,从而使调用者能将各个Future与Callable关联起来,所有任务执行完成时,或者调用线程被中断时,又或者超过指定时限,invokeAll()方法将返回。超过指定时限后,任务还未完成的任务都会被取消,invokeAll 返回后,每个任务只有两种状态,已完成或已取消,客户端代码可以调用get或者isCancelled来判断。

     

设限线程池大小

      可以根据Runtime.avaiableProcessors来动态计算。

      任务一般可分为:CPU密集型、IO密集型、混合型,对于不同类型的任务需要分配不同大小的线程池。

      对于CPU密集型任务 尽量使用较小的线程池,一般为CPU核心数+1,因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,只能增加上下文切换的次数,因此会带来额外的开销。

      IO密集型任务 可以使用稍大的线程池,一般为2*CPU核心数+1,IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候去处理别的任务,充分利用CPU时间。

      混合型任务 可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。 只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。 因为如果划分之后两个任务执行时间相差甚远,那么先执行完的任务就要等后执行完的任务,最终的时间仍然取决于后执行完的任务,而且还要加上任务拆分与合并的开销,得不偿失。

       下面介绍几个常用的参数:

      QPS:每秒查询率,是一台服务器每秒能够相应的查询次数,是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准。

      TPS:  TPS (transaction per second)代表每秒执行的事务数量,可基于测试周期内完成的事务数量计算得出。

           1)用户请求服务器

              2)服务器自己的内部处理

              3)服务器返回给用户

              这三个过程,每秒能够完成N个这三个过程,Tps也就是3;Qps基本类似于Tps,  但是不同的是,对于一个页面的一次访问,形成一个Tps;但一  次页面请求,可能产生多次对   服务器的请求,服务器对这些请求,就可计入Qps”之中。例如:访问一个页面会请求服务器3  次,一次放,产生一“T”,产生3个“Q”

       并发数 系统同时处理的request/事务数

       系统吞吐量一个系统的吞度量(承压能力)与request对CPU的消耗、外部接口、IO等等紧密关联。单个reqeust 对CPU消耗越高,外部系统接口、IO影响速度越慢,系统吞吐能力越低,反之越高。系统吞吐量几个重要参数:QPS(TPS)、并发数、响应时间,每套系统这两个值都有一个相对极限值,在应用场景访问压力下,只要某一项达到系统最高值,系统的吞吐量就上不去了,如果压力继续增大,系统的吞吐量反而会下降,原因是系统超负荷工作,上下文切换、内存等等其它消耗导致系统性能下降。

      它们之间的关系:
      QPS(TPS)= 并发数/平均响应时间    或者   并发数 = QPS*平均响应时间

      假设要求一个系统的TPS(Transaction Per Second或者Task Per Second)至少为20,然后假设每个Transaction由一个线程完成,继续假设平均每个线程处理一个Transaction的时间为4s。那么问题转化为:

      如何设计线程池大小,使得可以在1s内处理完20个Transaction?

      计算过程很简单,每个线程的处理能力为0.25TPS,那么要达到20TPS,显然 需要20/0.25=80个线程。

这个理论上成立的,但是实际情况中,一个系统最快的部分是CPU,所以决定一个系统吞吐量上限的是CPU。增强CPU处理能力,可以提高系统吞吐量上限。在考虑时需要把CPU吞吐量加进去。在IO优化文档中,有这样地公式:

最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目

即线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。 

但根据短板效应,真实的系统吞吐量并不能单纯根据CPU来计算。那要提高系统吞吐量,就需要从“系统短板”(比如网络延迟、IO)着手:

      1、尽量提高短板操作的并行化比率,比如多线程下载技术

      2、增强短板能力,比如用NIO替代IO

第一条可以联系到Amdahl定律,这条定律定义了串行系统并行化后的加速比计算公式:

       加速比=优化前系统耗时 / 优化后系统耗时

加速比越大,表明系统并行化的优化效果越好。Addahl定律还给出了系统并行度、CPU数目和加速比的关系,加速比为Speedup,系统串行化比率(指串行执行代码所占比率)为F,CPU数目为N:

       Speedup <= 1 / (F + (1-F)/N)

当N足够大时,串行化比率F越小,加速比Speedup越大。

这时候又抛出是否线程池一定比但线程高效的问题?

答案是否定的,比如Redis就是单线程的,但它却非常高效,基本操作都能达到十万量级/s。从线程这个角度来看,部分原因在于:

      1、多线程带来线程上下文切换开销,单线程就没有这种开销

      2、锁

       Redis很快更本质的原因在于: Redis基本都是内存操作,这种情况下单线程可以很高效地利用CPU。而多线程适用场景一般是:存在相当比例的IO和网络操作。

线程池中线程的创建和销毁

        线程池基本大小:就是线程池的目标大小,即在没有任务执行时线程池的大小,并且只有在工作队列满了的情况下才会创建超过这个数量的线程。线程池最大大小:表示可以同时活动的线程数量的上限。在线程池中,请求都会交由Executor管理的Runnable队列中等待,通过一个Runable和一个链表节点来表现一个等待中的任务。

        很多公司都不建议或者强制不允许直接使用 Executors 类提供的方法来创建线程池,例如阿里巴巴Java开发手册里就明确不允许这样创建线程池,一定要通过 ThreadPoolExecutor(xx,xx,xx...) 来明确线程池的运行规则,指定更合理的参数。先来看一下 ThreadPoolExecutor 的几个参数和它们的意义,先来看一下它最完整参数的重载。

corePoolSize

核心线程数,当有任务进来的时候,如果当前线程数还未达到 corePoolSize 个数,则创建核心线程,核心线程有几个特点:

      1、当线程数未达到核心线程最大值的时候,新任务进来,即使有空闲线程,也不会复用,仍然新建核心线程;

      2、核心线程一般不会被销毁,即使是空闲的状态,但是如果通过方法 allowCoreThreadTimeOut(boolean value) 设置为 true 时,超时也同样会被销毁;

      3、生产环境首次初始化的时候,可以调用 prestartCoreThread() 方法来预先创建所有核心线程,避免第一次调用缓慢;

maximumPoolSize:

       除了有核心线程外,有些策略是当核心线程完全无空闲的时候,还会创建一些临时的线程来处理任务,maximumPoolSize 就是核心线程 + 临时线程的最大上限。临时线程有一个超时机制,超过了设置的空闲时间没有事儿干,就会被销毁。

 

keepAliveTime:

       超时时间,也就是线程的最大空闲时间,默认用于非核心线程,通过 allowCoreThreadTimeOut(boolean value) 方法设置后,也会用于核心线程。

Unit:

       这个参数配合上面的 keepAliveTime ,指定超时的时间单位,秒、分、时等。

workQueue

      等待执行的任务队列,如果核心线程没有空闲的了,新来的任务就会被放到这个等待队列中。这个参数其实一定程度上决定了线程池的运行策略,为什么这么说呢,因为队列分为有界队列、无界队列,另外还有同步移交。

       有界队列:队列的长度有上限,当核心线程满载的时候,新任务进来进入队列,当达到上限,有没有核心线程去即时取走处理,这个时候,就会创建临时线程。(需要警惕临时线程无限增加的风险)

      无界队列:队列没有上限的,当没有核心线程空闲的时候,新来的任务可以无止境的向队列中添加,而永远也不会创建临时线程。(需要警惕任务队列无限堆积的风险)

      同步移交:使用SynchronousQueue来避免任务排队,直接将任务由生产者交给消费者,适用于非常大或者无界的线程池,SynchronousQueue并不是一个真正的队列,newCacheThreadPool中就用到了SynchronousQueue0.

      如果使用了LinkedBlockingQueue或ArrayBlockingQueue这样的FIFQ队列时,任务的执行顺序和他们到达的顺序一致,如果想要进一步控制任务的执行顺序,可以使用PriorityBlockingQueue,这个队列会根据优先级来安排任务,任务的优先级是通过自然顺序或者Comparator来定义的。

      只有当任务相互独立的时候,为线程池或工作队列设置界限才合理,如果任务之间存在依赖性,那么有界的线程池或者队列就可能会导致线饥饿死锁,此时应该使用无界的线程池,比如newCacheThreadPool。

threadFactory

      它是一个接口,用于实现生成线程的方式、定义线程名格式、是否后台执行等等,可以用 Executors.defaultThreadFactory() 默认的实现即可,也可以用 Guava 等三方库提供的方法实现,如果有特殊要求的话可以自己定义。它最重要的地方在于定义线程名称的格式,便于排查问题

Handler

当没有空闲的线程处理任务,并且等待队列已满(只对有界队列有效),再有新任务进来的话,就要做一些取舍了,而这个参数就是指定取舍策略的,有下面四种策略可以选择:

      ThreadPoolExecutor.AbortPolicy:直接抛出异常,这是默认策略;       ThreadPoolExecutor.DiscardPolicy:直接丢弃任务,但是不抛出异常。       ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后将新来的任    务加入等待队列

      ThreadPoolExecutor.CallerRunsPolicy:由线程池所在的线程处理该   任务,比如在 main 函数中创建线程池,如果执行此策略,将有 main 线程来执行该任务,该线程池可以实现平缓性能降低。

并不提倡用 Executors 中的方法来创建线程池,但还是用他们来讲一下几种线程池的原理。

1、newFixedThreadPool 

它有两个重载方法,代码如下:

创建固定线程数量线程池, corePoolSize 和 maximumPoolSize 要一致,即核心线程数和最大线程数(核心+非核心线程)一致,Executors 默认使用的是 LinkedBlockingQueue 作为等待队列,这是一个无界队列,这也是使用它的风险所在,除非你能保证提交的任务不会无节制的增长,否则不要使用无界队列,这样有可能造成等待队列无限增加,造成 OOM

不用上述方法创建固定线程数线程池的做法是

上面代码是创建一个 5 个线程的固定数量线程池,这里线程存活时间没有作用,所以设置为 0,使用了 ArrayBlockingQueue 作为等待队列,设置长度为 10 ,最多允许10个等待任务,超过的任务会执行默认的 AbortPolicy 策略,也就是直接抛异常。ThreadFactory 使用了 Guava 库提供的方法,定义了线程名称,方便之后排查问题。

2newSingleThreadExecutor

建立一个只有一个线程的线程池,如果有超过一个任务进来,只有一个可以执行,其余的都会放到等待队列中,如果有空闲线程,则在等待队列中获取,遵循先进先出原则。使用 LinkedBlockingQueue 作为等待队列。

这个方法同样存在等待队列无限长的问题,容易造成 OOM,所以正确的创建方式参考上面固定数量线程池创建的方式,只是把 poolSize 设置为 1

3newCachedThreadPool

缓存型线程池,在核心线程达到最大值之前,有任务进来就会创建新的核心线程,并加入核心线程池,即时有空闲的线程,也不会复用。达到最大核心线程数后,新任务进来,如果有空闲线程,则直接拿来使用,如果没有空闲线程,则新建临时线程。并且线程的允许空闲时间都很短,如果超过空闲时间没有活动,则销毁临时线程。关键点就在于它使用 SynchronousQueue 作为等待队列,它不会保留任务,新任务进来后,直接创建临时线程处理,这样一来,也就容易造成无限制的创建线程,造成 OOM

正确的创建缓存型线程池的做法是

4、newScheduledThreadPool

计划型线程池,可以设置固定时间的延时或者定期执行任务,同样是看线程池中有没有空闲线程,如果有,直接拿来使用,如果没有,则新建线程加入池。使用的是 DelayedWorkQueue 作为等待队列,这中类型的队列会保证只有到了指定的延时时间,才会执行任务。

 

 

下图是ThreadPoolExecutor 执行逻辑的总结:

 

扩展ThreadPoolExecotor

       ThreadPoolExecotor是可以扩展的,它提供了几个可以在子类中重写的方法:

beforeExecute()afterExecute()terminated()。在执行任务的线程中会调用beforeExecute()afterExecute()等,可以在这些方法中添加日志、计时、监视或统计信息等功能。无论线程是从run中正常返回还是抛出一个异常返回,afterExecute()均会被调用,但如果任务完成后带了一个error则不会调用afterExecute(),如果beforeExecute()抛出了一个RuntimeExecption,则任务不会执行,afterExecute()也不会被调用。线程池关闭的时候调用了terminated()方法,释放Executor在其生命周期内分配的各种资源,还能执行发送通知、记录日志或收集finalize统计信息等操作。

打印出的日志信息如下:

 

SpringBoot中线程池的简化和使用

       SpringbootThreadPoolExecutor进行了简化,只需要配置一个java.util.concurrent.TaskExecutor或者其子类的bean,并且在配置类或者程序入口类上声明注解@EnableAsync调用也简单,在由Spring管理的对象的方法上标注注解@Async,显式调用即可生效。

       声明:

 

调用:

测试:

测试结果:

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值