Java线程池的坑

背景

最近在优化代码(把一个大任务变成使用多线程分批执行小任务),使用多线程首当其冲就是使用线程池,一般比较常用的就是Executors.newFixedThreadPool,毕竟有现成的就用是菜鸟的一贯风格,然而却不知道已经掉入坑中了。

现象

优化完代码后,自测是一个好的开发必做的事情,毕竟好的开发不应该让测试太劳累,首先来常规的边界值测试,把每一批执行的量设置为1,尽可能的模拟多批次,在某个界面操作完后,再次点其中操作特定的界面,发现卡在那里,前端等待后端响应。(内心os:我可真是个写bug小能手)

排查

一般前端卡在那里,基本就是数据库连接池不用够用,java线程池被占满。

  1. 首先排查是否是数据库连接池的问题,连接数据库show processlist一下,你想看的不想看的都能看到,发现都是sleep,睡得可香呢,不打扰,不打扰。
  2. 接下来,矛头直指java线程池,为了方便排查,作为写外挂小能手的我,二话不说就写一个接口,用于查看线程池里的线程的情况,主要就是看activeCount和completedTaskCount。

复现

万事俱备,只等再来一次,解决问题的前提是复现问题,按照流程又来了一次,果不其然,又卡在那里了,看了一下我的接口,activeCount被占得死死的,小样你的锅跑不掉了。

分析

Executors 是一个Java中的工具类。提供工厂方法来创建不同类型的线程池。newFiexedThreadPool(int Threads):创建固定数目线程的线程池。
咋一看好像没毛病,JDK自身提供的构建线程池的方式,又用到了工厂模式、又有比较强的扩展性,重要的是用起来还比较方便,多重光环加身,让你无法说不。

lazy val threadPool = Executors.newFixedThreadPool(20)

创建一个固定大小的线程池就这么简单,省时省力省心。

但是知人知面不知心,让我们进一步查看源码。

/**
     * 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 <= 0}
     */
    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

LinkedBlockingQueue成功的引起了我的注意,这个是个啥玩意,点进去一看。

/**
     * Creates a {@code LinkedBlockingQueue} with a capacity of
     * {@link Integer#MAX_VALUE}.
     */
    public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
    }

Java中的BlockingQueue主要有两种实现,分别是ArrayBlockingQueue 和 LinkedBlockingQueue。

ArrayBlockingQueue是一个用数组实现的有界阻塞队列,必须设置容量。

LinkedBlockingQueue是一个用链表实现的有界阻塞队列,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE。

这里的问题就出在:不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE。也就是说,如果我们不设置LinkedBlockingQueue的容量的话,其默认容量将会是Integer.MAX_VALUE。

而newFixedThreadPool中创建LinkedBlockingQueue时,并未指定容量。此时,LinkedBlockingQueue就是一个无边界队列,对于一个无边界队列来说,是可以不断的向队列中加入任务的,这种情况下就有可能因为任务过多而导致内存溢出问题。

以上的是大坑,以下的是小坑

  • 任务提交后长时间没有执行
    任务进入了队列,线程还在执行之前的任务。本质原因是对线程和队列的优先级认识不深刻,有一种错觉以为是所有线程都忙的时候才进入任务队列。实际上相反,是队列满的时候才会新建线程(线程数大于core size时)。
  • 线程池中线程执行任务中无故消失(从日志可以看出,任务并未完成,也没有抛出异常)
    一般情况下,代码中只会去捕捉RuntimeException,如果抛出Error则会导致线程退出,而异常信息又没有拿到。最佳的解决办法是给线程池设置UncaughtExceptionHandler

解决

首先让我看其他三种创建线程池的方式:newSingleThreadExecutor、newCachedThreadPool、newScheduledThreadPool,要不就是无边界队列导致OOM,要不就是创建的最大线程数导致OOM,都有坑,只能自定义创建线程池了。写个方法方便使用。

/**
    * 不使用系统自带的四个Executors,有坑,轻则线程中任务没有执行,重则OOM导致系统崩掉
    * 使用有边界队列,超出队列的情况返回给调用者执行
    *
    * @param nThreads
    * @return
    */
  def newFixedThreadPool(nThreads: Int): ThreadPoolExecutor = {
    new ThreadPoolExecutor(nThreads, nThreads, 3L, TimeUnit.SECONDS, new ArrayBlockingQueue(nThreads),
      Executors.defaultThreadFactory, new ThreadPoolExecutor.CallerRunsPolicy)
  }

corePoolSize- 核心池大小。需要注意的是在初创建线程池时线程不会立即启动,直到有任务提交才开始启动线程并逐渐时线程数目达到corePoolSize。若想一开始就创建所有核心线程需调用prestartAllCoreThreads方法。

maximumPoolSize-池中允许的最大线程数。需要注意的是当核心线程满且阻塞队列也满时才会判断当前线程数是否小于最大线程数,并决定是否创建新线程。

keepAliveTime - 当线程数大于核心时,多于的空闲线程最多存活时间

unit - keepAliveTime 参数的时间单位。

workQueue - 当线程数目超过核心线程数时用于保存任务的队列。主要有3种类型的BlockingQueue可供选择:无界队列,有界队列和同步移交。从参数中可以看到,此队列仅保存实现Runnable接口的任务。 别看这个参数位置很靠后,但是真的很重要,有的坑就因这个参数而起,这些细节有必要仔细了解清楚。

threadFactory - 执行程序创建新线程时使用的工厂。

handler - 阻塞队列已满且线程数达到最大值时所采取的饱和策略。java默认提供了4种饱和策略的实现方式:中止、抛弃、抛弃最旧的、调用者运行。

总结

不要使用系统自带的四个Executors,有坑,轻则线程中任务没有执行,重则OOM导致系统崩掉,使用自定义创建线程池,参数根据实际的场景设置。

参考资料

Java线程池使用的注意事项
深入源码分析Java线程池的实现原理
一次Java线程池误用引发的血案和总结
Java中线程池,你真的会用吗?

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270