线程池的滥用造成应用挂了.....


前言

接手了一个项目,用了 ThreadPoolExecutor自定义了线程池,然后项目中异步的任务就都采用它来运行~然而,在众多的异步任务中,有那么一个任务是会开启无头浏览器的。这个东西是占资源的,周末数量量大的时候,大量线程都执行了此任务,造成CPU、内存资源占用迅速飙升,从而影响到其他正常服务,应用挂了......

解决方式

都是异步任务了,此等占用资源的服务,采用1~2两个线程去跑就完事情了,严格控制某个时刻打开无头浏览器的线程数目。于是重新开一个较小的线程池,专门跑该服务。

扯扯5个线程池

前四种线程池本质上都是用的ThreadPoolExecutor,通过对ThreadPoolExecutor的7个参数的灵活配置,达到不同的效果。这7个参数如下:

int corePoolSize,	    // 核心线程池数目
int maximumPoolSize,	// 线程池线程存在的最大数据
long keepAliveTime,		// 除了核心线程、其他线程空闲时的存活时间
TimeUnit unit,			// 时间单位
//	阻塞队列,线程满时,此队列可以继续存放对应的任务
BlockingQueue<Runnable> workQueue,	
//	线程工厂, 专门用来创建线程, 可以通过实现ThreadFactory接口自定义一个线程工厂(包括自定义线程名称)
ThreadFactory threadFactory,       
//	线程池满且阻塞队列到达边界时,使用对应的拒绝策略来处理接下来的请求                  
RejectedExecutionHandler handler

5中线程池如下,第5个是JDK1.8添加的,采用的是ForkJoinPool进行实现,该线程池思想可以参考归并算法,其适合解决计算类型(CPU密集型)的任务场景。

IO场景也是可以的,不过可能“IO读”的场景比较适合,之前查库时,由于查询的数量量大,通过此方式进行“分治-合并”的查询
  • newFixedThreadPool:创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待
public static ExecutorService newFixedThreadPool(int nThreads) {
    
    
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
  • newSingleThreadExecutor:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行
public static ExecutorService newSingleThreadExecutor() {
    
    
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
  • newCachedThreadPool:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
  • newScheduledThreadPool:创建一个定长线程池,支持定时及周期性任务执行
public ScheduledThreadPoolExecutor(int corePoolSize) {
    
    
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }
  • newWorkStealingPool:JDK1.8增加的一个线程池,直译过来时工作窃取线程池,其实就是当有多个任务时,先完成的任务可以合并其他的线程的的执行结果。
public static ExecutorService newWorkStealingPool() {
    
    
        return new ForkJoinPool
            (Runtime.getRuntime().availableProcessors(),
             ForkJoinPool.defaultForkJoinWorkerThreadFactory,
             null, true);
    }

四种拒绝策略

关于拒绝的方式,JDK提供了RejectedExecutionHandler接口,我们也可以实现该接口自定义拒绝方式,而在JDK中则提供了四种RejectedExecutionHandler的实现。

  • AbortPolicy:该方式是直接抛出异常,也是默认的实现方式。
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    
    
            throw new RejectedExecutionException("Task " + r.toString() +
                    " rejected from " +
                    e.toString());
        }
  • CallerRunsPolicy:该策略适合于任务的的重要程度是比较高的。当线程池满的时候,会由调用线程池的线程去行任务。
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    
    
            if (!e.isShutdown()) {
    
    
                r.run();
            }
        }
  • DiscardPolicy:放弃策略,什么都不做也不说
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    
    
        }
  • DiscardOldestPolicy:放弃追来策略
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    
    
            if (!e.isShutdown()) {
    
    
                e.getQueue().poll();
                e.execute(r);
            }
        }

异常处理

某个线程中断, ThreadPoolExecutor如何处理?这里需要分为两种情况进行讨论,一种是submit提交任务,一种是通过execute的方式,直接执行任务。如图:
在这里插入图片描述
直接报错:
在这里插入图片描述
如果通过submit方式提交任务,你会发现没有错误信息进行输出。
在这里插入图片描述
此时,可以换成这种方式,通过future里头的get方法h=取出异常:
在这里插入图片描述
究其原因我们可以对比源码,在execute里头,在runWorker方法源码里头,我们可以看到task.run()这里。

对于execute方式来说,直接就是跑ThreadDemo1实现了Runnable的run方法,然后抛出异常,catch住后,继续抛出,然后执行finally里头的afterExecute方法,该方法默认中是没有具体实现的。

try {
    
    
            while (task != null || (task = getTask()) != null) {
    
    
                w.lock();
                // If pool is stopping, ensure thread is interrupted;
                // if not, ensure thread is not interrupted.  This
                // requires a recheck in second case to deal with
                // shutdownNow race while clearing interrupt
                if ((runStateAtLeast(ctl.get(), STOP) ||
                     (Thread.interrupted() &&
                      runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                    wt.interrupt();
                try {
    
    
                    beforeExecute(wt, task);
                    Throwable thrown = null;
                    try {
    
    
                        task.run(); // 执行任务
                    } catch (RuntimeException x) {
    
    
                        thrown = x; throw x;
                    } catch (Error x) {
    
    
                        thrown = x; throw x;
                    } catch (Throwable x) {
    
    
                        thrown = x; throw new Error(x);
                    } finally {
    
    
                        afterExecute(task, thrown);
                    }
                } finally {
    
    
                    task = null;
                    w.completedTasks++;
                    w.unlock();
                }
            }
            completedAbruptly = false;
        } finally {
    
    
            processWorkerExit(w, completedAbruptly);
        }

对于submit来说,由于经过一层封装,类似适配器模式,此时来到task.run()是,这个task看似是个Runnablele类型,其本质是FutureTask,此时执行的是FutureTask里的run方法。
在这里插入图片描述
如图,Runnable经过适配器,已经和callable实现转换。
在这里插入图片描述
最终,把异常设置进去FutureTask中,完成异常的传递。
在这里插入图片描述

线程“坏了”咋办

线程如果出现了了异常,那线程池还要怎样处理它呢?我们都知道线程池的一个好处就是线程复用了,那线程“坏了”如出异常了,线程池如何处置呢?

从源码我们可以得出线程坏了的时候,它会执行这段源码processWorkerExit(w, completedAbruptly),如下,通过workers.remove(w)移除线程,然后又通过addWorker重新新建线程~

怎样移除呢?原来所有的work线程都是通过一个HashSet进行保存的,而HashSet的移除又是用的hashMap方法,最后的原理还是跟HashMap一致,将value置为了null,那key是谁呢?当然就是Worker线程对象本身。

ps:关于key这里,小伙伴们还会想到哪些知识点呢?

private void processWorkerExit(Worker w, boolean completedAbruptly) {
    
    
        if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted
            decrementWorkerCount();

        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
    
    
            completedTaskCount += w.completedTasks;
            // 移除线程
            workers.remove(w);
        } finally {
    
    
            mainLock.unlock();
        }
        tryTerminate();
        int c = ctl.get();
        if (runStateLessThan(c, STOP)) {
    
    
            if (!completedAbruptly) {
    
    
                int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
                if (min == 0 && ! workQueue.isEmpty())
                    min = 1;
                if (workerCountOf(c) >= min)
                    return; // replacement not needed
            }
            // 添加线程
            addWorker(null, false);
        }
    }

关于线程池的大小设置

之前也是各种网上找资料,很多都是如下的设置方案,但只能说在无法判断具体场景的使用情况时,你可以尝试这么设置。实际效果还是需要结合用户访问情况、服务器配置情况来进行设置。

  • CPU密集型任务主要消耗CPU资源进行计算, 当任务为CPU密集型时,核心池线程数设置为CPU核数+1

  • IO密集型一般设置为CPU核心两倍,
    总线程数量可以设置为核心线程两倍。

以目前接受的项目的使用情况,在4C8G的服务器配置下,核心线程设置为20的情况下,效果甚加,其他线程大小也设置为20(考虑到某个时刻下,核心线程全部挂了,此时我还有最多20个线程可以创建)

ps: 拓展:为何IO密集型一般设置为CPU核心两倍? 关键词:超线程

猜你喜欢

转载自blog.csdn.net/legendaryhaha/article/details/107878614