聊聊線程池,這一頓操作說懵阿里面試官...

想要進(jìn)階自己的開發(fā)水平蜕便,JDK源碼中一些優(yōu)秀的設(shè)計必須要經(jīng)常學(xué)習(xí)劫恒,哪怕不學(xué)習(xí),應(yīng)對面試的時候轿腺,還是要能夠應(yīng)對幾招两嘴,代表自己對這些東西還是有所了解。

而線程池的源碼族壳,這塊更是面試中經(jīng)常被問到的東西憔辫,先試著列幾個問題,看看自己對線程池的掌握程度:

  1. 創(chuàng)建線程池的參數(shù)有哪些螺垢,分別代表什么意思?

  2. 為什么阿里要求不能直接使用Executors工具類創(chuàng)建線程池孽亲?

  3. 線程池線程的數(shù)量如何配置栖茉?

  4. 一般線程池提交任務(wù),執(zhí)行任務(wù)的過程吼虎?

  5. 線程池中ctl屬性的作用是什么?

  6. 線程池的狀態(tài)有哪些歹颓?在什么時候下會出現(xiàn)?

  7. 一般線程池中有哪些未實現(xiàn)的空方法隔节,可以用做線程池的擴(kuò)展?

  8. 線程池中每一個具體的worker線程什么時候開始執(zhí)行?執(zhí)行的過程是什么强胰?

  9. 核心線程與非核心線程在線程池中是怎么區(qū)分的距糖?

  10. 線程池中的那個方法可以提前創(chuàng)建核心線程恩脂?

  11. 什么情況下worker線程會退出典阵?

  12. 核心線程會不會退出歹啼?

  13. 由于程序異常導(dǎo)致的退出和線程池內(nèi)部機(jī)制導(dǎo)致的退出有什么區(qū)別?

  14. 線程池shutdown與shutdownNow有什么區(qū)別屡限?

image.png

對上面問題都已經(jīng)了如指掌的大佬炕倘,聯(lián)系我,讓我表達(dá)對你的膜拜... 以上問題相對來說并不是很難罩旋,只要有認(rèn)真看線程池源碼,都可以找到答案瓜饥。然后以后有人再問你線程池相關(guān)問題時东帅,就可以拿出來說自己對線程池的理解,來聊聊把...

郵箱:aihe.ah@alibaba-inc.com
微信:aihehe5211

常見問題

使用線程池有哪些好處帐我?

首先在開發(fā)的過程中愧膀,為什么需要線程池呢?給我們帶來了那些好處

  • 提高系統(tǒng)的響應(yīng)速度
  • 如果每次多線程操作都創(chuàng)建一個線程檩淋,會浪費時間和消耗系統(tǒng)資源,而線程池可以減少這些操作
  • 可以對多個線程進(jìn)行統(tǒng)一管理媚朦,統(tǒng)一調(diào)度氧敢,提高線程池的可管理性

創(chuàng)建線程池的參數(shù)有哪些?

線程池是怎么創(chuàng)建的呢询张?一個是使用Executors,另外就是手動創(chuàng)建線程池份氧,要了解其每個參數(shù)的含義。Executors創(chuàng)建線程池的話恋拷,要不就是對線程的數(shù)量沒有控制厅缺,如CachedThreadPool,要不就是是無界隊列阎抒,如FixedThreadPool消痛。對線程池數(shù)量和隊列大小沒有限制的話都哭,容易導(dǎo)致OOM異常。所以我們要自己手動創(chuàng)建線程池:

  • corePoolSize:核心線程數(shù)量纱新,默認(rèn)情況下每提交一個任務(wù)就會創(chuàng)建一個核心線程穆趴,直到核心線程的數(shù)量等于corePoolSize就不再創(chuàng)建未妹。線程池提供了兩個方法可以提前創(chuàng)建核心線程,prestartAllCoreThreads()提前創(chuàng)建所有的核心線程络它,prestartCoreThread化戳,提前創(chuàng)建一個核心線程
  • maximumPoolSize:線程池允許創(chuàng)建的最大線程數(shù)。只有當(dāng)線程池隊列滿的時候才會創(chuàng)建
  • keepAliveTime:線程池空閑狀態(tài)可以等待的時間扫尖,默認(rèn)對非核心線程生效,但是設(shè)置allowCoreThreadTimeOut的話對核心線程也生效
  • unit: 被徊溃活時間的單位狰域,創(chuàng)建線程池的時候,keepAliveTime = unit.toNanos(keepAliveTime)
  • workQueue: 任務(wù)隊列兆览,用于保持或等待執(zhí)行的任務(wù)阻塞隊列抬探。BlockingQueue的實現(xiàn)類即可,有無界隊列和有界隊列
    • ArrayBlockingQueue: 基于數(shù)組結(jié)構(gòu)的有界隊列线梗,此隊列按FIFO原則對元素進(jìn)行排序
    • LinkedBlockingQueue: 基于鏈表的阻塞隊列怠益,F(xiàn)IFO原則,吞吐量通常高于ArrayBlockingQueue.
    • SynchronousQueue: 不存儲元素的阻塞隊列亚茬。每個插入必須要等到另一個線程調(diào)用移除操作抢呆。
    • PriorityBlockingQueue: 具有優(yōu)先級的無阻塞隊列
  • threadFactory: 用于設(shè)置創(chuàng)建線程的工廠抱虐。
  • handler:拒絕策略,當(dāng)隊列線程池都滿了恳邀,必須采用一種策略來處理還要提交的任務(wù)谣沸。在實際應(yīng)用中,我們可以將信息記錄到日志鳄抒,來分析系統(tǒng)的負(fù)載和任務(wù)丟失情況JDK中提供了4中策略:
    • AbortPolicy: 直接拋出異常
    • CallerRunsPolicy: 只用調(diào)用者所在的線程來運行任務(wù)
    • DiscardOldestPolicy: 丟棄隊列中最老的一個人任務(wù),并執(zhí)行當(dāng)前任務(wù)瓤鼻。
    • DiscardPolicy: 直接丟棄新進(jìn)來的任務(wù)

線程池提交任務(wù)的過程茬祷?

可以使用兩個方法執(zhí)行任務(wù):

  • execute() 提交不需要返回值的任務(wù),無法判斷是否執(zhí)行成功祭犯,具體步驟上面我們有分析
  • submit() 提交有返回值的任務(wù)沃粗,該方法返回一個future的對象,通過future對象可以判斷任務(wù)是否執(zhí)行成功突雪。future的get方法會阻塞當(dāng)前線程直到任務(wù)完成涡贱。
    • submit內(nèi)部使用RunnableFuture對任務(wù)進(jìn)行封裝

整體分為三個步驟:

  1. 判斷當(dāng)前線程數(shù)是否小于corePoolSize,如果小于督函,則新建核心線程激挪,不管核心線程是否處于空閑狀態(tài)
  2. 核心線程創(chuàng)建滿之后,后續(xù)的任務(wù)添加到workQueue中
  3. 如果workQueue滿了,則開始創(chuàng)建非核心線程直到線程的總數(shù)為maximumPoolSize
  4. 當(dāng)非核心線程數(shù)也滿了锋喜,隊列也滿了的時候豌鸡,執(zhí)行拒絕策略

中間會有一些對當(dāng)前線程池的檢查操作。

image-20200212084315747.png

線程池數(shù)量如何配置炉奴?

  • 任務(wù)性質(zhì):CPU密集蛇更,IO密集赛糟,和混合密集
  • 任務(wù)執(zhí)行時間:長璧南,中师逸,低
  • 任務(wù)優(yōu)先級:高,中动知,低
  • 任務(wù)的依賴性:是否依賴其它資源员辩,如數(shù)據(jù)庫連接

在代碼中可以通過:Runtime.getRuntime().availableProcessors();獲取CPU數(shù)量。線程數(shù)計算公式:

N = CPU數(shù)量
U = 目標(biāo)CPU使用率拆讯,  0 <= U <= 1
W/C = 等待(wait)時間與計算(compute)時間的比率

線程池數(shù)量 =  N * U * (1 + W/C)

不過最簡單的線程數(shù)指定方式养叛,不需要公式的話:

  • CPU密集型,創(chuàng)建線程數(shù)為CPU核數(shù) + 1
  • IO密集型爽室,線程數(shù)最好為CPU核數(shù) * n淆攻,耗時越久,分配線程數(shù)多一些

線程池的狀態(tài)有哪些啸箫?

線程池的狀態(tài)主要通過ctl屬性來控制伞芹,通過ctl可以計算出:

  • 當(dāng)前線程池狀態(tài)
  • 當(dāng)前線程的數(shù)量

計算規(guī)則主要是利用了按位操作:

11100000000000000000000000000000   RUNNING
00000000000000000000000000000000   SHUTDOWN
00100000000000000000000000000000   STOP
01000000000000000000000000000000   TYDYING
01100000000000000000000000000000   TERMINATED


11100000000000000000000000000000   ctl初始值
11100000000000000000000000000000  ~CAPACITY  
private static int runStateOf(int c)     { return c & ~CAPACITY; }

11100000000000000000000000000000   ctl初始值
00011111111111111111111111111111  CAPACITY
private static int workerCountOf(int c)  { return c & CAPACITY; }
    
private static int ctlOf(int rs, int wc) { return rs | wc; }  
  • RUNNING:運行狀態(tài)唱较,接受新任務(wù),持續(xù)處理任務(wù)隊列里的任務(wù)胸遇。
  • SHUTDOWN:調(diào)用shutdown()方法會進(jìn)入此狀態(tài)汉形,不再接受新任務(wù)倍阐,但要處理任務(wù)隊列里的任務(wù)
  • STOP:調(diào)用shutdownNow()方法收捣,不再接受新任務(wù)庵楷,不再處理任務(wù)隊列里的任務(wù),中斷正在進(jìn)行中的任務(wù)
  • TIDYING:表示線程池正在停止運作咐蚯,中止所有任務(wù)弄贿,銷毀所有工作線程。
  • TERMINATED:表示線程池已停止運作期奔,所有工作線程已被銷毀危尿,所有任務(wù)已被清空或執(zhí)行完畢
    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
    private static final int COUNT_BITS = Integer.SIZE - 3;
    private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

    // runState is stored in the high-order bits
    private static final int RUNNING    = -1 << COUNT_BITS;
    private static final int SHUTDOWN   =  0 << COUNT_BITS;
    private static final int STOP       =  1 << COUNT_BITS;
    private static final int TIDYING    =  2 << COUNT_BITS;
    private static final int TERMINATED =  3 << COUNT_BITS;

關(guān)于TIDYING和TERMINATED主要有一塊代碼區(qū)谊娇,可以看出來TIDYING狀態(tài)緊接著就是TERMINATED。

                        if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
                    try {
                            // 默認(rèn)是空方法
                        terminated();
                    } finally {
                        ctl.set(ctlOf(TERMINATED, 0));
                        termination.signalAll();
                    }
                    return;
                }

線程池提供的擴(kuò)展方法有哪些赠堵?

默認(rèn)有三個擴(kuò)展方法法褥,可以用來做一些線程池運行狀態(tài)統(tǒng)計,監(jiān)控:

 protected void beforeExecute(Thread t, Runnable r) { }  // task.run方法之前執(zhí)行
 protected void afterExecute(Runnable r, Throwable t) { }  // task執(zhí)行完之后揍愁,不管有沒有異常都會執(zhí)行
 protected void terminated() { }  

默認(rèn)線程池也提供了幾個相關(guān)的可監(jiān)控屬性:

  • taskCount: 線程池需要執(zhí)行的任務(wù)數(shù)量
  • completedTaskCount: 已經(jīng)完成的任務(wù)數(shù)量
  • largestPoolSize: 線程池中曾經(jīng)創(chuàng)建的最大的線程數(shù)量
  • getPoolSize: 線程池的線程數(shù)量
  • getActiveCount: 活動的線程數(shù)

線程池中的Worker線程執(zhí)行的過程酱鸭?

Worker類實現(xiàn)了Runnable方法垛吗,在成功創(chuàng)建Worker線程后就會調(diào)用其start方法。

w = new Worker(firstTask);
final Thread t = w.thread;   //理解為 w.thread = new Thread(w)
if (workerAdded) {
    t.start();
    workerStarted = true;
}


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

Worker線程運行時執(zhí)行runWorker方法蔚舀,里面主要事情:

  • 如果構(gòu)造Worker的時候,指定了firstTask狼牺,那么首先執(zhí)行firstTask礼患,否則從隊列中獲取任務(wù)
  • Worker線程會循環(huán)的getTask(),然后去執(zhí)行任務(wù)
  • 如果getTask()為空悄泥,那么worker線程就會退出
  • 在任務(wù)執(zhí)行前后肤粱,可以自定義擴(kuò)展beforeExecute與afterExecute方法
  • 如果檢測到線程池為STOP狀態(tài),并且線程還沒有被中斷過的話鸥鹉,進(jìn)行中斷處理

簡單來說就是不斷的從任務(wù)隊列中取任務(wù)庶骄,如果取不到瓢姻,那么就退出當(dāng)前的線程,取到任務(wù)就執(zhí)行任務(wù)幻碱。

    final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); // allow interrupts
        // 代表著Worker是否因為用戶的程序有問題導(dǎo)致的死亡
        boolean completedAbruptly = true;
        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 (Exception x) {
                                                  //... 不同的異常處理
                    } finally {
                        afterExecute(task, thrown);
                    }
                } finally {
                    task = null;
                    w.completedTasks++;
                    w.unlock();
                }
            }
            completedAbruptly = false;
        } finally {
            processWorkerExit(w, completedAbruptly);
        }
    }

線程池如何區(qū)分核心線程與非核心線程儡嘶?

實際上內(nèi)部在創(chuàng)建線程時恍风,并沒有給線程做標(biāo)記,因此無法區(qū)分核心線程與非核心線程凯楔。可以看出addWorker()方法摆屯。

但是為什么可以保持核心線程一直不被銷毀呢糠亩?

其內(nèi)部主要根據(jù)當(dāng)前線程的數(shù)量來處理准验。也可以理解為廷没,只要當(dāng)前的worker線程數(shù)小于配置的corePoolSize颠黎,那么這些線程都是核心線程。線程池根據(jù)當(dāng)前線程池的數(shù)量來判斷要不要退出線程盏缤,而不是根據(jù)是否核心線程

核心線程能否被退出唉铜?

上面一個問題我們說到了內(nèi)部其實不區(qū)分核心線程與非核心線程的,只是根據(jù)數(shù)量來判斷是否退出線程竞惋,但是線程是如何退出的灰嫉,又是如何一直處于保活狀態(tài)呢浑厚?

如果配置了allowCoreThreadTimeOut根盒,代表核心線程在配置的keepAliveTime時間內(nèi)沒獲取到任務(wù),會執(zhí)行退出操作敢艰。也就是盡管當(dāng)前線程數(shù)量小于corePoolSize也會執(zhí)行退出線程操作册赛。

workQueue.take()方法會一直阻塞當(dāng)前的隊列直到有任務(wù)的出現(xiàn),因此如果執(zhí)行的是take方法牡属,那么當(dāng)前的線程就不會退出扼睬。想要退出當(dāng)前的線程,有幾個條件:

  • 1 當(dāng)前的worker數(shù)量大于maximumPoolSize的worker數(shù)量证芭。
  • 2 線程池當(dāng)前處于STOP狀態(tài)担映,即shutdownNow
  • 3 線程池處于SHUTDOWN狀態(tài)蝇完,并且當(dāng)前的隊列為空
  • 4 worker線程等待task超時了,并且當(dāng)前的worker線程配置為可以被退出氢架。timed=true
    • allowCoreThreadTimeOut配置為true
    • 線程數(shù)量大于核心線程數(shù)
image-20200212092008155.png

如何提前創(chuàng)建核心線程數(shù)朋魔?

上面提到了警检,有兩個方法:

  • prestartAllCoreThreads()提前創(chuàng)建所有的核心線程
  • prestartCoreThread,提前創(chuàng)建一個核心線程拓售,如果當(dāng)前線程數(shù)量大于corePoolSize镶奉,則不創(chuàng)建

線程池異常退出與自動退出的區(qū)別?

如果線程是由于程序異常導(dǎo)致的退出鸽凶,那么completedAbruptly為true移国,如下代碼會再新建一個Worker線程。

如果線程是系統(tǒng)自動退出使碾,即completedAbruptly為false的話祝懂,會根據(jù)配置判斷當(dāng)前可以允許的最小核心線程數(shù)量

  • 配置allowCoreThreadTimeOut為true的話,最小核心線程數(shù)可以為0矢门。
  • 默認(rèn)情況下最小線程數(shù)為corePoolSize
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);
        }

線程池shutdown與shutdownNow有什么區(qū)別祟剔?

看代碼主要三個區(qū)別:

  • shutdown會把線程池的狀態(tài)改為SHUTDOWN,而shutdownNow把當(dāng)前線程池狀態(tài)改為STOP
  • shutdown只會中斷所有空閑的線程宣旱,而shutdownNow會中斷所有的線程叛薯。
  • shutdown返回方法為空,會將當(dāng)前任務(wù)隊列中的所有任務(wù)執(zhí)行完畢组力;而shutdownNow把任務(wù)隊列中的所有任務(wù)都取出來返回抖拴。
image-20200212093527721.png

最后

學(xué)無止境阿宅,還有很多細(xì)節(jié),但足以打動面試官脱柱,覺得真是一個很用心的候選人呢... 希望這些能幫到你拉馋。

還有阿里內(nèi)推:這邊是阿里集團(tuán)-淘系技術(shù)部的,總裁帶頭發(fā)起項目随闺,新成立部門蔓腐,業(yè)務(wù)急速擴(kuò)張,目前還有大量的HC散罕,機(jī)會多傀蓉,考慮的聯(lián)系我:

郵箱:aihe.ah@alibaba-inc.com

微信:aihehe5211

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末葬燎,一起剝皮案震驚了整個濱河市缚甩,隨后出現(xiàn)的幾起案子窑邦,更是在濱河造成了極大的恐慌冈钦,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異驾窟,居然都是意外死亡认轨,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進(jìn)店門恩急,熙熙樓的掌柜王于貴愁眉苦臉地迎上來衷恭,“玉大人纯续,你說我怎么就攤上這事〈翱矗” “怎么了倦炒?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵逢唤,是天一觀的道長。 經(jīng)常有香客問我遂唧,道長吊奢,這世上最難降的妖魔是什么纹烹? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任铺呵,我火速辦了婚禮隧熙,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘音念。我一直安慰自己躏敢,他們只是感情好件余,可當(dāng)我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著旬渠,像睡著了一般端壳。 火紅的嫁衣襯著肌膚如雪损谦。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天觅捆,我揣著相機(jī)與錄音麻敌,去河邊找鬼。 笑死赢赊,一個胖子當(dāng)著我的面吹牛级历,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播玩讳,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼熏纯,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了误窖?” 一聲冷哼從身側(cè)響起秩贰,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎毒费,沒想到半個月后丙唧,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡蝗罗,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了蝌戒。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片串塑。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖北苟,靈堂內(nèi)的尸體忽然破棺而出桩匪,到底是詐尸還是另有隱情,我是刑警寧澤傻昙,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站彩扔,受9級特大地震影響妆档,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜虫碉,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一贾惦、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧敦捧,春花似錦须板、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至秽荤,卻和暖如春甜奄,著一層夾襖步出監(jiān)牢的瞬間柠横,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工贺嫂, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留滓鸠,地道東北人。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓第喳,卻偏偏與公主長得像糜俗,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子曲饱,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容