Java之ThreadPool

该文是本人的学习总结,若有错误,望指正;下文所有分析都基于JDK-8

本文不做源代码的分析,因为有大量的文章已经这样做了,并且做得很漂亮,比如这篇。这里只是想大概梳理一下ThreadPool设计的思路。

ThreadPool的状态参数

JDK的线程池定义了五个状态参数,如下代码所示;

//正常状态:处理任务并接收新的任务
private static final int RUNNING    = -1 << COUNT_BITS;

//关闭状态:处理BlockingQueue中的任务,但不再接收新的任务
//若有新的任务继续到来,如何处理?请看下文
private static final int SHUTDOWN   =  0 << COUNT_BITS;

//停止状态:不再接收新任务,也不再处理BlockingQueue中的任务;
//不仅如此,还会中断正在进行中的任务
private static final int STOP       =  1 << COUNT_BITS;

//姑且称为整理状态吧,这个状态的ThreadPool会调用钩子方法 terminated()
private static final int TIDYING    =  2 << COUNT_BITS;

//结束状态:terminated()方法完成后就是这个状态了
private static final int TERMINATED =  3 << COUNT_BITS;

需要注意的是,这些值是多少不重要,重要的是它们的相对大小;因为程序中是使用它们的相对大小来进行逻辑判断;采用这种移位来进行定义,只是便于程序利用位运算来进行计算;

初始化一个ThreadPool时,它的状态就是 RUNNING 的;

上面实际上就是一个状态机,那么这些状态如何进行转移?

  • RUNNING -> SHUTDOWN:调用shutdown(), 很熟悉的方法;
  • (RUNNING or SHUTDOWN) -> STOP:调用shutdownNow();
  • SHUTDOWN -> TIDYING:不是主动的触发,而是当BlockingQueue和线程池里线程数都为0的时候,可以把此时的状态记为TIDYING;
  • STOP -> TIDYING:线程池里没有线程了,记状态为TIDYING;
  • TIDYING -> TERMINATED:terminated()结束;

线程的生命周期

我不想知道线程是怎么来的,只想知道线程是怎么没的

日常中,我们都使用线程池来执行任务。但是作为计算机工作者,追根溯源应该是我们最起码的一个学习态度,^-^。

当有新的任务(Task)被提交(execute)到线程池里时,会被包装到Worker类里。Worker里重要的属性如下:

//真正执行任务的线程
final Thread thread;

//待执行的任务,可为空
Runnable firstTask;

新建线程

线程池会根据coolPoolSize、maximumPoolSize和BlockingQueue中任务的数量来决定是否新建线程;刚开始的时候肯定是要新建线程的,每新建一个线程(使用ThreadFactory创建),实际就是new 一个Worker,并把新的线程放在其属性thread中;

线程的保活

之所以使用线程池,是因为线程的创建,管理等比较麻烦,交给线程池来做,一劳永逸;那就有一个问题了,线程池中的部分线程如何保证是一直存活的?这样才能高效得服务使用者。

线程池中的线程和我们自己创建的线程一个较大的区别在于:当线程池中的线程运行完一个任务后,不是被立刻回收(结束)了,而是去BlockingQueue中获取任务,获取到新的任务,就执行这个任务;获取不到,才干掉或者阻塞(blocked)线程阻塞实际上就是线程保活的一个重要手段(这里可以参考线程池的getTask()方法);熟悉BlockingQueue原理的朋友,肯定知晓阻塞的线程实际上是被park了,若queue非空,就会unpark这些线程。

线程的销毁

闲暇时,线程池中只会保留coolPoolSize的线程,多余的线程都会被干掉;当然,线程池被shutdown时,所有线程都会被干掉的;

举个例子,coolPoolSize=5,maximumPoolSize=10,BlockingQueue的capacity=20;当任务很多时,池里的线程很可能会扩充到10;那么任务执行完后,如何干掉多余的5个?这里要参考runWorker()方法了, 下述代码省略了一些;

final void runWorker(Worker w) {
        try {
            while (task != null || (task = getTask()) != null) {
                w.lock(); //获取锁
                beforeExecute(); //钩子
                task.run();
                afterExecute();//钩子
                w.unlock();
            }
            completedAbruptly = false;
        } finally {
            processWorkerExit(w, completedAbruptly);
        }
}

while循环不结束,当前线程就无法结束;getTask()会根据线程池的设置返回null或task;返回null,while结束,线程可以回收;当getTask()判断线程数已经不大于coolPoolSize,那么这些剩余的线程如上文所述,就会阻塞。

几个问题

  • 线程池刚诞生时就会创建 coolPoolSize个线程吗?
    :不是。而是随着任务的到来,逐渐新建线程;

  • 内部类Worker为何设计为一个锁,并且是不可重入的?
    :线程池暴露了一些设置参数的方法,比如setCorePoolSize()等;这些参数会影响到一些线程的生命周期;不妨看一下setCorePoolSize()的方法体:

public void setCorePoolSize(int corePoolSize) {
        ......
        if (workerCountOf(ctl.get()) > corePoolSize)
            interruptIdleWorkers();
        else if (delta > 0) {
            .......
            while (k-- > 0 && addWorker(null, true)) {
                if (workQueue.isEmpty())
                    break;
            }
        }
    }
private void interruptIdleWorkers(boolean onlyOne) {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            for (Worker w : workers) {
                Thread t = w.thread;
                if (!t.isInterrupted() && w.tryLock()) { //这里会获取锁
                    try {
                        t.interrupt(); //中断线程
                    } catch (SecurityException ignore) {
                    } finally {
                        w.unlock();
                    }
                }
                if (onlyOne)
                    break;
            }
        } finally {
            mainLock.unlock();
        }
    }

个人理解,线程池中的线程要么在运行,要么在阻塞;当一个线程在运行时,尽量不要中断这个线程;所以这里的非重入锁是为了禁止中断运行中的线程而设计,尽管目前运行的线程根本就没有响应这个中断;假如不是一个锁,或者是一个可重入的锁,那么interrupt()极有可能中断一个正在运行的线程(这个线程的中断标志位会被set),本人觉得这样不会影响线程的运行,但是按照java api doc描述,除非线程池即将停止,否则不要给正常的线程设置中断标志位;

Before running any task, the lock is acquired to prevent other pool interrupts
while the task is executing, and then we ensure that unless pool is stopping, 
this thread does not have its interrupt set.

本文参考了 这里

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

推荐阅读更多精彩内容

  • 1)从此刻起: 我要多鼓励、赞美孩子, 而不是批评、指责、埋怨孩子。 因为我知道只有鼓励和赞美才能带给孩子自信和力...
    satsukhkaur阅读 329评论 0 0
  • 这个蓝是大牌今年新出的色系。YSL
    李葳Taiwan阅读 291评论 0 0
  • 午后明媚的阳光让人心里一阵暖意 这种惬意的时刻戴上耳机听着音乐喝着咖啡脑袋里面浮现出你那甜美的笑容 我总是会想你那...
    阿昕小章鱼阅读 268评论 0 3
  • Don't be concerned with the distance of the journey, ever...
    浮生一梦_阅读 227评论 1 1