Java线程池详解3--线程池终止

线程池终止主要依靠以下2个命令:

  • shutdown()
  • shutdownNow()

首先看一下shutdown方法:

shutdown

public void shutdown() {
    final ReentrantLock mainLock = this.mainLock;
    // 获取独占锁
    mainLock.lock();
    try {
        // 检查各worker是否可操作
        checkShutdownAccess();
        // 将线程池状态更新为SHUTDOWN
        advanceRunState(SHUTDOWN);
        // 中断空闲线程
        interruptIdleWorkers();
        // 响应shutdown操作,由ThreadPoolExecutor的继承类来具体实现
        onShutdown();
    } finally {
        mainLock.unlock();
    }
    // 执行tryTerminate()操作
    tryTerminate();
}

advanceRunState

private void advanceRunState(int targetState) {
    // 阻塞执行
    for (;;) {
        int c = ctl.get();
        // 1. 若当前线程池状态>=targetState,直接break
        // 2. 将线程池的状态更新为targetState,并在ctl中保留当前的工作线程数,若成功,则直接break
        if (runStateAtLeast(c, targetState) ||
            ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c))))
            break;
    }
}

执行完advanceRunState方法后,当前线程池状态肯定已>=targetState。

interruptIdleWorkers

该方法用于中断线程池中的空闲线程。

在前面系列文章中我们讲过,worker在执行任务前会先获取锁,执行完任务则释放锁,所以处于锁定状态的worker(state为1)为工作线程,而处于无锁状态的worker(state为0)为空闲线程。

private void interruptIdleWorkers() {
    interruptIdleWorkers(false);
}

private void interruptIdleWorkers(boolean onlyOne) {
    final ReentrantLock mainLock = this.mainLock;
    // 获取线程池独占锁
    mainLock.lock();
    try {
        for (Worker w : workers) {
            Thread t = w.thread;
            // 若当前worker持有的线程未被中断过,且获取worker锁成功,则执行线程中断操作
            // 若获取worker锁不成功,证明该线程为工作线程,不执行线程中断操作
            if (!t.isInterrupted() && w.tryLock()) {
                try {
                    t.interrupt();
                } catch (SecurityException ignore) {
                } finally {
                    w.unlock();
                }
            }
            // 仅执行1次即退出
            if (onlyOne)
                break;
        }
    } finally {
        mainLock.unlock();
    }
}

待线程池状态更新为shutdown,且所有空闲线程被中断,则执行onShutdown方法来响应shutdown操作。

上述过程执行完之后,执行tryTerminate()操作来终止线程池。

tryTerminate

final void tryTerminate() {
    for (;;) {
        int c = ctl.get();
        // 出现下述3种情况,直接return:
        // 1. 当前线程池处于running状态
        // 2. 当前线程池处于tidying或terminated状态
        // 3. 当前线程池处于shutdown状态,且workQueue不为空,此时只是拒绝提交新任务,但workQueue中的任务还需要继续执行完
        if (isRunning(c) ||
            runStateAtLeast(c, TIDYING) ||
            (runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty()))
            return;
        // 代码走到这,说明为以下2种情况:
        // 1. 当前线程池处于stop状态
        // 2. 当前线程池处于shutdown状态,且workQueue为空
        if (workerCountOf(c) != 0) {
            // 中断唤醒1个正在等任务的空闲worker
            interruptIdleWorkers(ONLY_ONE);
            return;
        }
        
        // 此时,线程池状态为shutdown,workQueue为空,且正在运行的worker也没有了,开始terminated()
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            // 将线程池状态更新为tidying
            if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
                try {
                    // 此方法由线程池子类来定义
                    terminated();
                } finally {
                    // 将线程池状态更新为terminated
                    ctl.set(ctlOf(TERMINATED, 0));
                    termination.signalAll();
                }
                return;
            }
        } finally {
            mainLock.unlock();
        }
    }
}

假设如下情景:

当发出shutdown命令时,线程池还有3个核心工作线程,由于interruptIdleWorkers()仅会中断空闲线程,所以这3个核心工作线程不会被中断。

上述3个核心工作线程执行完自身携带和阻塞队列中的任务后,会变为空闲核心线程。由于此时线程状态已变为shutdown,所以会从runWorker()的while循环中(线程池状态为shutdown时,getTask会返回null)跳出,执行finally代码块中的processWorkerExit方法。

而processWorkerExit又会执行tryTerminate方法。所以tryTerminate会不断循环传递调用,tryTerminate方法的interruptIdleWorkers(ONLY_ONE)看起来仅是中断一个空闲线程,但其实这个中断信号会引发一个"循环中断风暴",直到线程池中所有worker被中断。

不得不说,Doug Lea是真大神呀!!!这种设计真是巧妙到极点!!!

梳理完shutdown整个代码后可以发现,线程池状态的变化过程如下:

running-->shutdown-->tidying-->terminated。

执行完shutdown,线程池状态首先会更新为shutdown,然后中断所有空闲线程,当剩余工作线程执行完持有的任务,且将阻塞队列中的任务也执行完毕,变为空闲线程时,执行tryTerminate()操作将线程池状态更新为tidying,待线程池完成terminated()操作后,线程池状态最终变为terminated。

趁热打铁,接着看一下shutdownNow方法:

shutdownNow

public List<Runnable> shutdownNow() {
    List<Runnable> tasks;
    final ReentrantLock mainLock = this.mainLock;
    // 获取线程池独占锁
    mainLock.lock();
    try {
        // 检查各worker是否可操作
        checkShutdownAccess();
        // 将线程池状态更新为STOP
        advanceRunState(STOP);
        // 尝试中断所有已启动的worker
        interruptWorkers();
        // 将阻塞队列中的任务清空
        tasks = drainQueue();
    } finally {
        mainLock.unlock();
    }
    // 执行tryTerminate()操作
    tryTerminate();
    // 返回任务集合
    return tasks;
}

interruptWorkers

private void interruptWorkers() {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        for (Worker w : workers)
            // 中断已启动的线程
            w.interruptIfStarted();
    } finally {
        mainLock.unlock();
    }
}

void interruptIfStarted() {
    Thread t;
    if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
        try {
            t.interrupt();
        } catch (SecurityException ignore) {
        }
    }
}

可以发现,中断worker时,会首先执行判断getState() >= 0,前面我们已经说过了,worker除了工作线程(state为1),就是空闲线程(state为0),那getState() >= 0恒成立呀,为啥多此一举呢?

其实worker刚初始化未启动前,其状态为-1,执行到runWorker时,会执行w.unlock()操作将状态修改为0,此时线程才可被中断。

方法如其名,interruptIfStarted仅能中断已启动的worker

Worker(Runnable firstTask) {
    setState(-1); // inhibit interrupts until runWorker
    this.firstTask = firstTask;
    this.thread = getThreadFactory().newThread(this);
}

drainQueue

private List<Runnable> drainQueue() {
    BlockingQueue<Runnable> q = workQueue;
    ArrayList<Runnable> taskList = new ArrayList<Runnable>();
    // 首先通过阻塞队列的drainTo方法将队列中的Runnable转移到taskList中
    q.drainTo(taskList);
    // 如果阻塞队列是DelayQueue或者阻塞队列执行poll或drainTo操作失败,则需要通过遍历的方法完成Runnable转移操作
    if (!q.isEmpty()) {
        for (Runnable r : q.toArray(new Runnable[0])) {
            if (q.remove(r))
                taskList.add(r);
        }
    }
    // 返回阻塞队列中的任务集合
    return taskList;
}

综上,drainQueue主要完成2种操作:

  • 清空阻塞队列中的元素;
  • 将阻塞队列中的元素保存到List中返回。

梳理完shutdownNow整个代码后可以发现,线程池状态的变化过程如下:

running-->stop-->tidying-->terminated。

执行完shutdownNow,线程池状态首先会更新为stop,接着中断所有已启动worker,然后执行tryTerminate()操作将线程池状态更新为tidying,待线程池完成terminated()操作后,线程池状态最终变为terminated。

可以发现,对线程池执行shutdown或shutdownNow后,线程池状态均需要一系列的流程才能将线程池状态更新为terminated。

那如何确认当前线程池已处于terminated状态呢?

线程池为此提供了awaitTermination方法:

awaitTermination

public boolean awaitTermination(long timeout, TimeUnit unit)
    throws InterruptedException {
    // 将时间换算为纳秒
    long nanos = unit.toNanos(timeout);
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        for (;;) {
            // 如果线程池状态已更新为terminated,则直接返回true
            if (runStateAtLeast(ctl.get(), TERMINATED))
                return true;
            // 若nanos <= 0,则直接返回false
            if (nanos <= 0)
                return false;
            // 阻塞等待nanos,并返回新的nanos
            nanos = termination.awaitNanos(nanos);
        }
    } finally {
        mainLock.unlock();
    }
}

所以线程池终止的最佳实践是:

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