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

您所在的位置:网站首页 线程池数量设置 谈谈线程池使用原则 (线程池如何监控)

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

#谈谈线程池使用原则 (线程池如何监控)| 来源: 网络整理| 查看: 265

 

基础知识

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

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

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

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

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

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

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

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

 

使用原则

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

 

总结

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

什么是线程池?

 

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

 

线程池的好处

 

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

 

线程池核心类

 

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

 

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

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大小的阻塞队列。当执行任务时,如果线程都很忙,就会丢到工作队列等有空闲线程时再执行,队列满就执行默认的拒绝策略。

 

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

 

newCachedThreadPool

       

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

 

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

 

newSingleThreadExecutor

       

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

 

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

 

newScheduledThreadPool

 

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

 

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

 

拒绝策略

 

AbortPolicy

 

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

 

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

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

 

CallerRunsPolicy

 

        

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

 

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

 

DiscardPolicy

 

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

 

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

 

DiscardOldestPolicy

 

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

 

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

 

如何提交线程

 

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

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是我们线程池核心类,首先看看线程池类的主要参数有哪些。

 

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

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大小的阻塞队列。当执行任务时,如果线程都很忙,就会丢到工作队列等有空闲线程时再执行,队列满就执行默认的拒绝策略。

 

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

 

newCachedThreadPool

       

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

 

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

 

newSingleThreadExecutor

       

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

 

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

 

newScheduledThreadPool

 

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

 

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

 

拒绝策略

 

AbortPolicy

 

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

 

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

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

 

CallerRunsPolicy

 

        

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

 

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

 

DiscardPolicy

 

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

 

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

 

DiscardOldestPolicy

 

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

 

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

 

如何提交线程

 

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

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类:

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

 

public static void main(String[] args) {

 

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

ExecutorService executorService1 = Executors.newFixedThreadPool(4);

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

ExecutorService executorService2 = Executors.newSingleThreadExecutor();

//创建缓存行线程池

ExecutorService executorService3 = Executors.newCachedThreadPool();

   

}

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

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

ThreadPoolExecutor类

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

 

/**

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

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

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

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

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

* If any thread terminates due to a failure during execution

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

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

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

*

* @param nThreads the number of threads in the pool

* @return the newly created thread pool

* @throws IllegalArgumentException if {@code nThreads



【本文地址】


今日新闻


推荐新闻


    CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3