《Java核心技术系列二》ThreadPoolExecutor 使用注意事项

该系列统一使用java8的源码进行讲解

上一篇中对ThreadPoolExecutor的源码以及工作原理进行了讲解。今天来讲解一下在使用的过程中我们应该注意哪些问题

一. 参数冲突问题

在上一篇文章中,我们对ThreadPoolExecutor的构造函数的参数进行了讲解。其中

  1. 当corePoolSize==maximumPoolSize,且核心线程数不允许超时时,设置keepAliveTime与unit是没有意义的(因为没有需要超时的Worker)
  2. 当workQueue为无界队列时,其永远也放不满,这种情况maximumPoolSize就不会生效

二. Exectors 提供的三种线程池能不能用

2.1. 固定大小的线程池
    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

看实现可知,线程数固定,且采用的是无界阻塞队列。当任务提交的速度快与Worker消耗任务的速度时,任务会在workQueue中不断挤压,最终撑垮整个进程的内存空间,造成OOM,因此线上谨慎使用

2.2 单个线程的线程池
    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

与1相比都是固定大小的线程池,唯一不同的是该线程数为1,存在于1一样的问题。

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

与固定大小的线程池正好相反,核心线程数为0,阻塞队列容量为1,有任务就开辟新的Worker执行,空闲时Worker会自动销毁。如果任务提交的任务非常的多,应用进程达到开辟线程的最大上限开辟不了线程抛出OOM,或者因为线程占用太多的内存空间,导致内存被耗尽抛出OOM(一个线程栈的默认大小为1m),或者因为线程太多,线程上下文切换的开销不可忽略,导致任务执行缓存从而拖垮整个应用进程。

综上:上面三种线程池都不推荐使用,而是应该由我们自己制定线程池参数,保证线程个数与任务队列的长度都在可控的范围内,而不是任由其无限制增长,在线上应用中,是不推荐存在这种不可控的情况出现的。资源都可控制了,那就延伸出另一种问题,达到了线程池的最大处理能力了怎么办?

三. 拒绝策略 RejectedExecutionHandler

当线程池达到自己最大处理能力时(任务队列满,每个Worker都在不停工作),再添加新任务时就会触发拒绝策略。JDK提供了4中拒绝策略

3.1. AbortPolicy 默认

直接拒绝,就是会抛出异常

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            throw new RejectedExecutionException("Task " + r.toString() +
                                                 " rejected from " +
                                                 e.toString());
        }

注意是谁抛出异常,是执行提交任务的线程抛出异常。如果提交任务的线程正在循环的处理一批数据,且没有对异常进行捕获处理,那么在执行拒绝策略抛出异常后,后面的任务都不会被执行。如果提交任务的线程是一个用户请求线程,且业务中没有捕获该异常进行处理,用户请求会因为该异常而请求失败。并且这是默认的拒绝策略,初学者很容易掉进这个坑

3.2 DiscardPolicy

直接丢弃,实现就是什么都不做

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}

针对不能对任务的场景是不能使用的

3.3 DiscardOldestPolicy

丢弃最老的一个任务

       public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                e.getQueue().poll();
                e.execute(r);
            }
        }

先从workQueue中弹出一个任务,然后再提交任务。同样会丢失数据,在不允许丢数据的场景不能使用

3.4 CallerRunsPolicy

调用者线程执行,谁提交任务,谁执行

        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                r.run();
            }
        }

通过直接调用任务run方法的方式自己执行,虽然会延长线程的执行时间,但是这样不会丢失数据

四. 线程池如何关闭

4.1 shutdown

将线程池的状态修改为SHUTDOWN状态,在该状态不允许提交新任务

    public void shutdown() {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess();
            //修改线程池的状态
            advanceRunState(SHUTDOWN);
            //终端空闲的worker
            interruptIdleWorkers();
            //空方法
            onShutdown(); // hook for ScheduledThreadPoolExecutor
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
    }

首先将线程池状态修改为shutdown状态,然后中断所有空闲的Worker。为什么这里需要中断线程呢,因为调用线程的interrupt方法,能够让阻塞在getTask中的take方法与poll方法上的Worker唤醒,唤醒之后,重新检查线程池的状态为shutdown状态,getTash方法就会返回null,从而Thread的run方法,线程销毁。最后调用tryTerminate尝试终止,只要有任务没有执行完成,有Worker在运行就不会转为terminate状态。看下tryTerimate的源码:

    final void tryTerminate() {
        for (;;) {
            int c = ctl.get();
            if (isRunning(c) ||
                runStateAtLeast(c, TIDYING) ||
                (runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty()))
                return;
            // stop 状态 或者shutdown状态,且workQueue没有内容
            if (workerCountOf(c) != 0) { // Eligible to terminate
                interruptIdleWorkers(ONLY_ONE);
                return;
            }
           // stop状态 或者shutdown状态,worker都已经销毁
            final ReentrantLock mainLock = this.mainLock;
            mainLock.lock();
            try {
                //将状态修改为tidying状态
                if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
                    try {
                        terminated();
                    } finally {
                        //将状态修改为terminate状态
                        ctl.set(ctlOf(TERMINATED, 0));
                        termination.signalAll();
                    }
                    return;
                }
            } finally {
                mainLock.unlock();
            }
            // else retry on failed CAS
        }
    }
  1. 首先判断状态,如果是running, tidying,terminate或者shutdown状态且workQueue不为空,就直接返回。总结就是:只有状态为stop 或者 shutdown且workQueue为空了,才能往下走。
  2. 如果Worker还存在,则中断一个空闲的worker,然后返回。
  3. 将状态修改为tidying状态,接着修改为terminate状态,在ThreadPoolExecutor中由于terminated方法时空方法,所以这两个状态可以认为是一样的。

什么样的状态才能转化为tidying状态, 首先前提是Worker都关闭了,其次是 stop,或者 shutdown且队列中没有任务了

4.2 shutdownNow

不同于shutdown的是,这里将状态修改为stop状态

    public List<Runnable> shutdownNow() {
        List<Runnable> tasks;
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess();
            advanceRunState(STOP);
            interruptWorkers();
            tasks = drainQueue();
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
        return tasks;
    }

首先将线程池状态修改为stop状态,然后唤醒所有的worker,然后调用tryTerminate方法,该方法已经讲过了。shutdownNow方法会返回阻塞队列中所有的任务。这些任务都没有被执行。

首先我们了解了shutdown与shutdownNow两个方法一个将状态修改为shutdown,一个将状态修改为stop,一个没有返回值,一直返回了workQueue中的任务列表。为了能够更加清晰的认清这两个方法,我们还是有必要再看一次getTask方法对于shutdown状态与stop状态有什么不同的处理操作。

// Check if queue empty only if necessary.
            if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
                decrementWorkerCount();
                return null;
            }

如果是shutdown状态,且任务队列中有任务,则继续执行。如果是stop状态,直接返回null,销毁worker。
总结:两个方法修改任务状态后都不允许再提交新的任务,shutdown方法将线程池的状态修改为shutdown状态,会等待所有已经提交的任务全部执行完成然后各个worker自动退出,shutdownNow不会执行完成所有的任务,但是会将没有执行完成的任务列表返回,其在getTask中会直接返回null也会引导各个worker自动退出。最终Worker全部销毁,整个线程池关闭

4.3 线程池都是事后自行退出,那是谁在修改线程池的状态到最终terminate状态?

这个问题的答案隐藏在Worker的run方法中,run方法调用了runWorker方法,在runWorker方法中,退出循环后会执行finnaly语句,在这里会调用processWorkerExit方法,而在这个方法中会调用tryTerminate方法,也就是说每有一个Worker执行完成,都会去执行下tryTerminate方法,尝试将线程池的状态修改为terminate状态。代码如下:

private void processWorkerExit(Worker w, boolean completedAbruptly) {
         ... 省略
        tryTerminate();
       ... 省略
}

在我们调用shutdown或者shutdownNow方法后,可以调用awaitTerminate方法,等待线程池关闭。

总结

针对ThreadPoolExecutor的使用事项,就先总结到这,大家如果有好的意见也可以提出来,我们一起探讨。

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

推荐阅读更多精彩内容