谈谈线程池使用原则 (线程池如何监控)

    https://mp.weixin.qq.com/s?__biz=MzUzODQ0MDY2Nw==&mid=2247483799&idx=1&sn=11e704259d87a16998aad986f4c673e4&chksm=fad6e723cda16e35d917fc10082a8de3fe00250c892f1a1cd5782bdc2b95997b601d068581e9&scene=0&ascene=7&devicetype=android-24&version=26060739&nettype=WIFI&abtest_cookie=BQABAAoACwANABIAEwAFACaXHgBPmR4AWZkeAICZHgCImR4AAAA=&lang=zh_CN&pass_ticket=371qk7f4kWmLX+Tvq8yhrowfCPVdV632gkYuVZAAgUhNDL8e9YZDu0dn4RQWbVeq&wx_header=1

https://www.jianshu.com/p/2c4c49e3a758

https://blog.csdn.net/u010185035/article/details/82846099

https://mp.weixin.qq.com/s/L2KKLlmOKJUQKfLdFa-1FA

基础知识


作为Java开发工程师,工作中基本上都会用到线程池。Java中线程池最基本的定义如下:

谈谈线程池使用原则 (线程池如何监控)_第1张图片

其中corePoolSize是线程池核心数目;maximumPoolSize线程池中线程的最大数目;keepAliveTime表示当线程数目大于core并且超过一定时间,会关闭多余的线程池;workQueue存放任务的线程;threadFactory创建线程的类;handler拒绝处理任务时的策略。

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

  1. 如果线程池里线程数量小于corePoolSize,不管线程池里面的线程是否处于运行状态,那么马上创建线程运行这个任务;

  2. 如果线程池里线程数量大于或等于corePoolSize,那么将这个任务放入队列。

  3. 如果这时候队列满了,而且线程池里的线程数目小于maximumPoolSize,那么还是要创建线程运行这个任务;

  4. 如果队列满了,并且线程池里的线程数目达到maximumPoolSize,那么线程池就会执行handler策略。

 

使用原则


一定要传递threadFactory这个参数,定义有意义的线程名

因为线程名有时候在排查问题的时候特别有用,比如:使用jstack,当整个线程栈看不出有用的信息,此时线程名就尤为关键了。每次看到poo-num-thread-num就想骂人。

谈谈线程池使用原则 (线程池如何监控)_第2张图片

另外有时候在配置日志的时候会输入线程名,此时有意义的线程名比毫无意义的默认线程名要好很多。

尽量避免局部变量创建线程池

引入线程池的目的提高资源复用,如果在局部变量创建线程池,基本上达不到提高资源复用,而且很有可能因为忘记调用shutdown出现资源泄漏。下面是一个这样的case:

谈谈线程池使用原则 (线程池如何监控)_第3张图片

上面的代码在多次执行之后将出现下面的OOM。谈谈线程池使用原则 (线程池如何监控)_第4张图片

线程池大小和队列设置原则

在谈论这个问题之前,我们先来看一个case。

有一次我们提供的服务接口时间慢慢上升,上升到一定时间之后不再上升,但是上游服务从我们这里获取不到数据了。而我们服务依赖的下游服务响应时间和数据确实正常的。最后排查下来发现在调用下游的时候使用了线程池。其中core=10,队列的size又特别大,下游服务接口的平均响应时间为100ms。那我们我们服务单机能够提供的最高QPS也就是100,当超过100的时候,任务进来之后会先在队列里等待。持续的处理能力跟不上,就会导致任务还没有执行,上游接口就超时了,拿不到数据。

所以对于核心接口以及没有突发流量情况下,我通过给出的建议是使用SynchronousQueue 这个队列,并且maxPoolSize尽量大一些。

当使用有界队列的时候,corePoolSize设置的应该尽可能和maximumPoolSize相等,并且针对队列应该设置监控。

还有可以根据任务特点来设置线程数。比如任务要是IO密集型线程池大小可以设置的大一些;要是CPU密集型设置小一点,可以简单设置为cpu ~ cpu *2。

最好能设计一个可监控的线程池

因为使用线程池有太多坑,特别是刚入门的新人,我司每年都会因为线程池问题发生的case。我认为要杜绝事故发生就是应该完善监控,在线程池使用不当时能够自动发现及时告警避免事故发生。

线程池监控的关键点,我认为以下几点:

  1. handler的监控。一旦任务进入handler说明此时线程池数目在max的时候都处理不过来了,服务肯定会收到影响。这种情况要及时处理。

  2. workQueue的大小。如果workQueue里面有挤压,说明线程数在core任务处理不过来,要注意这种情况对服务带来的影响。

  3. 监控activeCount的数目。这样可以了解设置的参数是否合理,比如core设置的太大,浪费资源。

  4. 监控通过线程池创建的线程总数。在创建线程时候+1,销毁的时候-1,这样可以监控是否有资源泄漏。

在完善监控之后,要是能做到动态调整线程池参数就更好了,比如发现任务进入了handler,可以动态调整max去处理挤压,处理完挤压之后再把max设置会原来的值。

 

总结


在使用线程的时候必须要仔细考量每个参数,以及可能带来的影响。并且还得考虑线程资源泄漏的问题。最好的情况下,公司能定义一个可监控的线程池组件,类似于hystrix。

什么是线程池?

 

很简单,简单看名字就知道是装有线程的池子,我们可以把要执行的多线程交给线程池来处理,和连接池的概念一样,通过维护一定数量的线程池来达到多个线程的复用。

 

线程池的好处

 

我们知道不用线程池的话,每个线程都要通过new Thread(xxRunnable).start()的方式来创建并运行一个线程,线程少的话这不会是问题,而真实环境可能会开启多个线程让系统和程序达到最佳效率,当线程数达到一定数量就会耗尽系统的CPU和内存资源,也会造成GC频繁收集和停顿,因为每次创建和销毁一个线程都是要消耗系统资源的,如果为每个任务都创建线程这无疑是一个很大的性能瓶颈。所以,线程池中的线程复用极大节省了系统资源,当线程一段时间不再有任务处理时它也会自动销毁,而不会长驻内存。

 

线程池核心类

 

在java.util.concurrent包中我们能找到线程池的定义,其中ThreadPoolExecutor是我们线程池核心类,首先看看线程池类的主要参数有哪些。

 

谈谈线程池使用原则 (线程池如何监控)_第5张图片

  • corePoolSize:线程池的核心大小,也可以理解为最小的线程池大小。

  • maximumPoolSize:最大线程池大小。

  • keepAliveTime:空余线程存活时间,指的是超过corePoolSize的空余线程达到多长时间才进行销毁。

  • unit:销毁时间单位。

  • workQueue:存储等待执行线程的工作队列。

  • threadFactory:创建线程的工厂,一般用默认即可。

  • handler:拒绝策略,当工作队列、线程池全已满时如何拒绝新任务,默认抛出异常。

 

线程池工作流程

 

1、如果线程池中的线程小于corePoolSize时就会创建新线程直接执行任务。

2、如果线程池中的线程大于corePoolSize时就会暂时把任务存储到工作队列workQueue中等待执行。

3、如果工作队列workQueue也满时:当线程数小于最大线程池数maximumPoolSize时就会创建新线程来处理,而线程数大于等于最大线程池数maximumPoolSize时就会执行拒绝策略。

 

线程池分类

 

Executors是jdk里面提供的创建线程池的工厂类,它默认提供了4种常用的线程池应用,而不必我们去重复构造。

 

  • newFixedThreadPool

     

     

    固定线程池,核心线程数和最大线程数固定相等,而空闲存活时间为0毫秒,说明此参数也无意义,工作队列为最大为Integer.MAX_VALUE大小的阻塞队列。当执行任务时,如果线程都很忙,就会丢到工作队列等有空闲线程时再执行,队列满就执行默认的拒绝策略。

 

谈谈线程池使用原则 (线程池如何监控)_第6张图片

 

  • newCachedThreadPool

       

       带缓冲线程池,从构造看核心线程数为0,最大线程数为Integer最大值大小,超过0个的空闲线程在60秒后销毁,SynchronousQueue这是一个直接提交的队列,意味着每个新任务都会有线程来执行,如果线程池有可用线程则执行任务,没有的话就创建一个来执行,线程池中的线程数不确定,一般建议执行速度较快较小的线程,不然这个最大线程池边界过大容易造成内存溢出。

 

谈谈线程池使用原则 (线程池如何监控)_第7张图片

 

  • newSingleThreadExecutor

       

       单线程线程池,核心线程数和最大线程数均为1,空闲线程存活0毫秒同样无意思,意味着每次只执行一个线程,多余的先存储到工作队列,一个一个执行,保证了线程的顺序执行。

 

 

  • newScheduledThreadPool

     

    调度线程池,即按一定的周期执行任务,即定时任务,对ThreadPoolExecutor进行了包装而已。

     

 

拒绝策略

 

  • AbortPolicy

 

      简单粗暴,直接抛出拒绝异常,这也是默认的拒绝策略。

 

谈谈线程池使用原则 (线程池如何监控)_第8张图片

 

  • CallerRunsPolicy

     

        

       如果线程池未关闭,则会在调用者线程中直接执行新任务,这会导致主线程提交线程性能变慢。

 

谈谈线程池使用原则 (线程池如何监控)_第9张图片

 

  • DiscardPolicy

 

       从方法看没做任务操作,即表示不处理新任务,即丢弃。

 

 

  • DiscardOldestPolicy

 

       抛弃最老的任务,就是从队列取出最老的任务然后放入新的任务进行执行。        

 

 

如何提交线程

 

如可以先随便定义一个固定大小的线程池

ExecutorService es = Executors.newFixedThreadPool(3);

 

提交一个线程

es.submit(xxRunnble);

es.execute(xxRunnble);

 

submit和execute分别有什么区别呢?

execute没有返回值,如果不需要知道线程的结果就使用execute方法,性能会好很多。

submit返回一个Future对象,如果想知道线程结果就使用submit提交,而且它能在主线程中通过Future的get方法捕获线程中的异常。

 

如何关闭线程池

 

e

什么是线程池?

 

很简单,简单看名字就知道是装有线程的池子,我们可以把要执行的多线程交给线程池来处理,和连接池的概念一样,通过维护一定数量的线程池来达到多个线程的复用。

 

线程池的好处

 

我们知道不用线程池的话,每个线程都要通过new Thread(xxRunnable).start()的方式来创建并运行一个线程,线程少的话这不会是问题,而真实环境可能会开启多个线程让系统和程序达到最佳效率,当线程数达到一定数量就会耗尽系统的CPU和内存资源,也会造成GC频繁收集和停顿,因为每次创建和销毁一个线程都是要消耗系统资源的,如果为每个任务都创建线程这无疑是一个很大的性能瓶颈。所以,线程池中的线程复用极大节省了系统资源,当线程一段时间不再有任务处理时它也会自动销毁,而不会长驻内存。

 

线程池核心类

 

在java.util.concurrent包中我们能找到线程池的定义,其中ThreadPoolExecutor是我们线程池核心类,首先看看线程池类的主要参数有哪些。

 

谈谈线程池使用原则 (线程池如何监控)_第10张图片

  • corePoolSize:线程池的核心大小,也可以理解为最小的线程池大小。

  • maximumPoolSize:最大线程池大小。

  • keepAliveTime:空余线程存活时间,指的是超过corePoolSize的空余线程达到多长时间才进行销毁。

  • unit:销毁时间单位。

  • workQueue:存储等待执行线程的工作队列。

  • threadFactory:创建线程的工厂,一般用默认即可。

  • handler:拒绝策略,当工作队列、线程池全已满时如何拒绝新任务,默认抛出异常。

 

线程池工作流程

 

1、如果线程池中的线程小于corePoolSize时就会创建新线程直接执行任务。

2、如果线程池中的线程大于corePoolSize时就会暂时把任务存储到工作队列workQueue中等待执行。

3、如果工作队列workQueue也满时:当线程数小于最大线程池数maximumPoolSize时就会创建新线程来处理,而线程数大于等于最大线程池数maximumPoolSize时就会执行拒绝策略。

 

线程池分类

 

Executors是jdk里面提供的创建线程池的工厂类,它默认提供了4种常用的线程池应用,而不必我们去重复构造。

 

  • newFixedThreadPool

     

     

    固定线程池,核心线程数和最大线程数固定相等,而空闲存活时间为0毫秒,说明此参数也无意义,工作队列为最大为Integer.MAX_VALUE大小的阻塞队列。当执行任务时,如果线程都很忙,就会丢到工作队列等有空闲线程时再执行,队列满就执行默认的拒绝策略。

 

谈谈线程池使用原则 (线程池如何监控)_第11张图片

 

  • newCachedThreadPool

       

       带缓冲线程池,从构造看核心线程数为0,最大线程数为Integer最大值大小,超过0个的空闲线程在60秒后销毁,SynchronousQueue这是一个直接提交的队列,意味着每个新任务都会有线程来执行,如果线程池有可用线程则执行任务,没有的话就创建一个来执行,线程池中的线程数不确定,一般建议执行速度较快较小的线程,不然这个最大线程池边界过大容易造成内存溢出。

 

谈谈线程池使用原则 (线程池如何监控)_第12张图片

 

  • newSingleThreadExecutor

       

       单线程线程池,核心线程数和最大线程数均为1,空闲线程存活0毫秒同样无意思,意味着每次只执行一个线程,多余的先存储到工作队列,一个一个执行,保证了线程的顺序执行。

 

 

  • newScheduledThreadPool

     

    调度线程池,即按一定的周期执行任务,即定时任务,对ThreadPoolExecutor进行了包装而已。

     

 

拒绝策略

 

  • AbortPolicy

 

      简单粗暴,直接抛出拒绝异常,这也是默认的拒绝策略。

 

谈谈线程池使用原则 (线程池如何监控)_第13张图片

 

  • CallerRunsPolicy

     

        

       如果线程池未关闭,则会在调用者线程中直接执行新任务,这会导致主线程提交线程性能变慢。

 

谈谈线程池使用原则 (线程池如何监控)_第14张图片

 

  • DiscardPolicy

 

       从方法看没做任务操作,即表示不处理新任务,即丢弃。

 

 

  • DiscardOldestPolicy

 

       抛弃最老的任务,就是从队列取出最老的任务然后放入新的任务进行执行。        

 

 

如何提交线程

 

如可以先随便定义一个固定大小的线程池

ExecutorService es = Executors.newFixedThreadPool(3);

 

提交一个线程

es.submit(xxRunnble);

es.execute(xxRunnble);

 

submit和execute分别有什么区别呢?

execute没有返回值,如果不需要知道线程的结果就使用execute方法,性能会好很多。

submit返回一个Future对象,如果想知道线程结果就使用submit提交,而且它能在主线程中通过Future的get方法捕获线程中的异常。

 

如何关闭线程池

 

es.shutdown(); 

不再接受新的任务,之前提交的任务等执行结束再关闭线程池。

 

es.shutdownNow();

不再接受新的任务,试图停止池中的任务再关闭线程池,返回所有未处理的线程list列表。

 

s.shutdown(); 

不再接受新的任务,之前提交的任务等执行结束再关闭线程池。

 

es.shutdownNow();

不再接受新的任务,试图停止池中的任务再关闭线程池,返回所有未处理的线程list列表。

 

Java线程池该如何监控?

日常开发中,当我们开发一些复合型任务时,尝尝会使用线程池通过异步的方式解决一些对时效性要求不高的任务。下面小编列举几种线程池的使用方式,以便参考!

Java JDK中默认封装好的Executors类:

下面简单的列举三种我们常用的线程池操作类:

 
  1. public static void main(String[] args) {

  2.  
  3. //创建大小为4个线程的线程池

  4. ExecutorService executorService1 = Executors.newFixedThreadPool(4);

  5. //创建一个单独线程的线程池

  6. ExecutorService executorService2 = Executors.newSingleThreadExecutor();

  7. //创建缓存行线程池

  8. ExecutorService executorService3 = Executors.newCachedThreadPool();

  9.  
  10.  
  11. }

这段代码的优点也是显而易见的,就是操作线程池的便利性,我们可以非常方便的使用线程池来结合到我们的业务开发中。 

但往往事物都两面性,这段代码的缺点就是可能导致OOM,因为其内部是一个无解队列,当你的任务数远远大于你的线程池数量时,缓存队列则会一直被追加,直到把你当前机器的内存塞满,最终导致OOM事件。

ThreadPoolExecutor类

根据Executors类的源码得知,内部其实是通过new ThreadPoolExecutor类进行实现的,下面我们来看下Executors.newFixedThreadPool的源码实现:

 
  1. /**

  2. * Creates a thread pool that reuses a fixed number of threads

  3. * operating off a shared unbounded queue. At any point, at most

  4. * {@code nThreads} threads will be active processing tasks.

  5. * If additional tasks are submitted when all threads are active,

  6. * they will wait in the queue until a thread is available.

  7. * If any thread terminates due to a failure during execution

  8. * prior to shutdown, a new one will take its place if needed to

  9. * execute subsequent tasks. The threads in the pool will exist

  10. * until it is explicitly {@link ExecutorService#shutdown shutdown}.

  11. *

  12. * @param nThreads the number of threads in the pool

  13. * @return the newly created thread pool

  14. * @throws IllegalArgumentException if {@code nThreads <= 0}

  15. */

  16. public static ExecutorService newFixedThreadPool(int nThreads) {

  17. return new ThreadPoolExecutor(nThreads, nThreads,

  18. 0L, TimeUnit.MILLISECONDS,

  19. new LinkedBlockingQueue());

  20. }

其他几种的Executors方法实现方式都大同小异,不一一列举。

回到主题,在日常中开发如何监控线程池呢,这时就需要我们刚才所说的ThreadPoolExecutor类。

通过ThreadPoolExecutor实现监控

其实监控线程池很简单,我们只要继承ThreadPoolExecutor就可以得到所有我们想要的,下面代码是继承后,所必须要重写几个构造函数重载。

 
  1. public class MyThreadPool extends ThreadPoolExecutor {

  2. public MyThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue) {

  3. super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);

  4. }

  5.  
  6. public MyThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory) {

  7. super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);

  8. }

  9.  
  10. public MyThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, RejectedExecutionHandler handler) {

  11. super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);

  12. }

  13.  
  14. public MyThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {

  15. super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);

  16. }

  17.  
  18. }

其实小编个人认为,最方便的还是通过重写execute、shutdown最为实用,可以达到我们监控或统一处理某些业务场景的实现,下面列举一些示例仅供参考:

 
  1. /**

  2. * 执行线程任务前执行

  3. *

  4. * @param t

  5. * @param r

  6. */

  7. @Override

  8. protected void beforeExecute(Thread t, Runnable r) {

  9. super.beforeExecute(t, r);

  10. }

  11.  
  12. /**

  13. * 执行线程时调用

  14. *

  15. * @param command

  16. */

  17. @Override

  18. public void execute(Runnable command) {

  19. //当前核心线程大小

  20. this.getCorePoolSize();

  21. //最大线程数大小

  22. this.getMaximumPoolSize();

  23. //当前线程池任务数量

  24. this.getTaskCount();

  25. //当前队列

  26. this.getQueue();

  27.  
  28.  
  29. //设置核心线程数量

  30. this.setCorePoolSize(4);

  31. //设置最大线程数

  32. this.setMaximumPoolSize(4);

  33.  
  34. super.execute(command);

  35. }

  36.  
  37. /**

  38. * 执行线程任务后执行

  39. *

  40. * @param r

  41. * @param t

  42. */

  43. @Override

  44. protected void afterExecute(Runnable r, Throwable t) {

  45. super.afterExecute(r, t);

  46. }

  47.  
  48. /**

  49. * 结束线程池时执行

  50. */

  51. @Override

  52. public void shutdown() {

  53. super.shutdown();

  54. }

通过上述代码可以得知,当继承ThreadPoolExecutor之后,我们可以方便的拿到当前线程池的coreSIze、maxiMumSize等等。这样我们不仅能够实时的去监控线程池的状态,同样可以通过setCorePoolSize等方法实现动态扩容,达到我们监控的目的。

其实我们也可以实时监控内存队列的大小,当达到某个预警值的时候进行报警,都可以很方便的实现。

总结:

小编本次不做深入的讲解,你希望帮助大家简单的认识下ThreadPoolExecutor的扩展方式,有什么问题也希望大家及时提问,欢迎可以共同探讨的同学,谢谢!

 

之前写过一篇 Java 线程池的使用介绍文章《线程池全面解析》,全面介绍了什么是线程池、线程池核心类、线程池工作流程、线程池分类、拒绝策略、及如何提交与关闭线程池等。

但在实际开发过程中,在线程池使用过程中可能会遇到各方面的故障,如线程池阻塞,无法提交新任务等。

如果你想监控某一个线程池的执行状态,线程池执行类ThreadPoolExecutor也给出了相关的 API, 能实时获取线程池的当前活动线程数、正在排队中的线程数、已经执行完成的线程数、总线程数等。

总线程数 = 排队线程数 + 活动线程数 +  执行完成的线程数。

下面给出一个线程池使用示例,及教你获取线程池状态。

privatestaticExecutorService es =newThreadPoolExecutor(50,100,0L, TimeUnit.MILLISECONDS,newLinkedBlockingQueue(100000));publicstaticvoidmain(String[] args)throwsException{for(inti =0; i <100000; i++) {        es.execute(() -> {            System.out.print(1);try{                Thread.sleep(1000);            }catch(InterruptedException e) {                e.printStackTrace();            }        });    }    ThreadPoolExecutor tpe = ((ThreadPoolExecutor) es);while(true) {        System.out.println();intqueueSize = tpe.getQueue().size();        System.out.println("当前排队线程数:"+ queueSize);intactiveCount = tpe.getActiveCount();        System.out.println("当前活动线程数:"+ activeCount);longcompletedTaskCount = tpe.getCompletedTaskCount();        System.out.println("执行完成线程数:"+ completedTaskCount);longtaskCount = tpe.getTaskCount();        System.out.println("总线程数:"+ taskCount);        Thread.sleep(3000);    }}

线程池提交了 100000 个任务,但同时只有 50 个线程在执行工作,我们每陋 3 秒来获取当前线程池的运行状态。

第一次程序输出:

当前排队线程数:99950

当前活动线程数:50

执行完成线程数:0

总线程数(排队线程数 + 活动线程数 +  执行完成线程数):100000

第二次程序输出:

当前排队线程数:99800

当前活动线程数:50

执行完成线程数:150

总线程数(排队线程数 + 活动线程数 +  执行完成线程数):100000

活动线程数和总线程数是不变的,排队中的线程数和执行完成的线程数不断在变化,直到所有任务执行完毕,最后输出:

当前排队线程数:0

当前活动线程数:0

执行完成线程数:100000

总线程数(排队线程数 + 活动线程数 +  执行完成线程数):100000

这样,你了解了这些 API 的使用方法,你想监控线程池的状态就非常方便了。

 

你可能感兴趣的:(多线程)