Java多线程:线程池的那些坑

一,前言

大家平时在工作会经常用到线程池进行多线程程序开发,正常做法,新建的线程直接丢到线程池里执行,然后就什么都不管了,一般情况下这样做也没什么错,但是在项目实战中我们吃了太多一般情况的亏,如果线程任务执行的业务逻辑比较耗时,又比如如果系统进行大促销,流量比较大的话,那么大概率(或者说基本)系统会因为线程的积压而导致内存被打爆,资源被吃光。那么究竟该怎样正确使用java的线程池?下面我们一起来讨论这个问题。

二,ExecutorService接口

JDK1.5开始,新增了ExecutorService接口,官方推荐使用这个接口进行线程池的创建。

2.1,Single线程池

//创建单个核心线程数线程池
ExecutorService singlePool = Executors.newSingleThreadExecutor();

具体实现代码:

public static ExecutorService newSingleThreadExecutor() {
    
    
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
}

corePoolSize和maximumPoolSize都设置的为1,但是阻塞队列LinkedBlockingQueue是无界的,这是内存被打爆的隐患,如果流量大,业务线程不断的被塞入,排队队列不断增加最终会导致内存资源耗尽。

2.2,fixed线程池

//创建固定数量线程数线程池
ExecutorService fixPool = Executors.newFixedThreadPool(50);

具体实现代码如下:

public static ExecutorService newFixedThreadPool(int nThreads) {
    
    
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
 }

fixed线程池线程就是N个线程数的Single线程池,和Single线程池有同样的隐患问题,不再赘述。

2.3,cache线程池

//创建无界线程池
ExecutorService cachePool = Executors.newCachedThreadPool();

具体实现代码如下:

public static ExecutorService newCachedThreadPool() {
    
    
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
 }

cache线程池的隐患就更大了,maximumPoolSize为 Integer.MAX_VALUE,可以理解为无限大,意味着可以不断塞入线程,并且排队队列也是没有限制,流量洪峰时内存被打爆风险更大。

2.4,线程池核心参数理解

以上三种方式创建线程,我们进入源码看到它都是用ThreadPoolExecutor类进行线程池的创建,我们看ThreadPoolExecutor类的构造方法,它接收以下六个参数,这就是构建线程池的核心参数

2.4.1,corePoolSize(核心线程数)

核心线程会一直存在,即使没有任务执行。当业务线程数小于核心线程数的时候,即使有空闲线程,也会一直创建线程直到达到核心线程数。设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭。

2.4.2,maximumPoolSize(最大线程数)

线程池里允许存在运行的最大业务线程数量。它和corePoolSize的实际意义,就比如一个篮球馆,正常情况可以容纳5000人看球,corePoolSize就是5000,当遇到热门比赛球馆临时加座一共涌进了8000个球迷看球,maximumPoolSize就是8000。

2.4.3,keepAliveTime(线程空闲时间)

当线程空闲时间达到keepAliveTime时,线程会退出(关闭),直到线程数等于核心线程数,如果设置了allowCoreThreadTimeout=true,则线程会退出直到线程数等于零。

2.4.4,TimeUnit(线程空闲时间的单位)

2.4.5,workQueue(任务队列容量)

也叫阻塞队列,当核心线程都在运行,此时再有任务进来,会进入任务队列,排队等待线程执行。

2.4.6,RejectedExecutionHandler(任务拒绝处理器)

当线程数量达到最大线程数,且任务队列已满时,会执行拒绝处理器。

三,四种线程池的拒绝策略

在使用线程池设定拒绝策略极端重要,一定要考虑线程池满了之后的处理逻辑,错误的使用了拒绝策略会造成业务逻辑上的漏洞。ThreadPoolExecutor类提供了四种内置策略:

3.1,AbortPolicy策略

丢弃任务,抛运行时异常

3.2,CallerRunsPolicy策略

执行任务

3.3,DiscardPolicy策略

忽视,什么都不会发生

3.4,DiscardOldestPolicy策略

从队列中踢出最先进入队列(最后一个执行)的任务

3.5,自定义

当然JDK也提供了自定义的拒绝策略,只要实现RejectedExecutionHandler接口。

四,正确的实践方法:

我建议大家这样使用线程池:

ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5, 20, 10,
            TimeUnit.SECONDS, new LinkedBlockingQueue<>(15), new LogRejectedExecutionHandler());

各个线程池的核心参数都自己设定,并且一定要自定义线程池拒绝策略,根据自己的业务逻辑编写拒绝逻辑,不然如果使用内置的拒绝策略,很容易造成业务逻辑上的漏洞。至于各个核心参数设定多少值,可根据实际机器的性能,cpu性能,cpu核心数等等因素,总之使用线程池,不断的调试观察机器性能变化,设置最合理的参数值和拒绝策略。

五,总结:

没有一劳永逸的解决方案,线程池顾名思义它是一个池子,要合理控制进入和流出的线程数量,并且设置合理拒绝策略。
好比篮球馆,它的吞吐量终归有上限的,要控制进入和走出球馆的人流量,进入的量大于出的量球馆就会人满为患,进入的量小于出的量也会造成球馆资源的浪费。当球馆人满为患时,要控制人员进入球馆的流量,并且要跟球迷解释原因,即拒绝策略。好了,啰嗦了这么多希望对大家有用,欢迎留言一起讨论。

猜你喜欢

转载自blog.csdn.net/datuanyuan/article/details/109097752