線程池你真的懂了嗎

閱讀任何源碼开伏,我們都應該帶著幾個問題去閱讀栅迄,從源碼中找出這些問題的答案,這樣才能徹底搞明白某個知識點蚕键。

下面我們就帶著這樣幾個問題鲫趁,一起看一下ThreadPoolExecutor的源碼

  1. 為什么要用線程池
  2. 為什么不推薦使用juc直接創(chuàng)建的線程池
  3. 線程池的幾個核心參數(shù)
  4. 線程池是什么時候創(chuàng)建線程的轴术?
  5. 線程池是如何重復利用線程的?
  6. 任務提交的順序和執(zhí)行的順序是一樣的嗎洗贰?

1寄猩、為什么要使用線程池

這個其實可以寫一個簡單的程序去跑一下嫉晶,比如使用線程池去跑1000個task和開1000個線程去跑這1000個task,線程池的效率會高出很多倍田篇,原因是線程池能夠重復利用線程,沒有創(chuàng)建和銷毀線程的開銷箍铭。
其實池化的技術在很多地方都會用到比如數(shù)據(jù)庫的連接池泊柬,字符串常量池,netty的對象池等等

2诈火、為什么不推薦使用juc直接創(chuàng)建線程池的方式

我們找兩個Exectors創(chuàng)建線程池的源碼

a兽赁、newCachedTreadPool的源碼

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

可以看到這里的最大線程數(shù)是0xffff個,這個最大線程數(shù)在大并發(fā)提交任務的情況下會創(chuàng)建大量線程冷守,會導致CPU100%

b刀崖、newSingleThreadExecutor

public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>(),
                                    threadFactory));
    }

//看一下LinkedBlockingQueue的實現(xiàn)
 public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
    }

會初始化一個容量為0xffff的隊列,由于這個隊列太大拍摇,如果我們提交的任務數(shù)很多并且自定義的線程里的對象又很大的話亮钦,就很容易發(fā)生oom的問題。

從這個工具類創(chuàng)建線程的參數(shù)我們可以看到底層調(diào)用的都是ThreadPoolExecutor的構造方法充活,所以我們建議根據(jù)具體業(yè)務的規(guī)模設置合適的線程池參數(shù)蜂莉。

new ThreadPoolExecutor()

3、線程池的幾個核心參數(shù)

ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) 

corePoolSize:最大線程數(shù)
maximumPoolSize:最大線程數(shù)
keepAiveTime:線程存活時間
TimeUnit:存活時間的參數(shù)
BlockingQueue:線程池的任務隊列

task投遞到線程池中的整個過程如下


image.png

看一下具體的代碼

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();

        //這里判斷線程數(shù)量是否小于corePoolSize混卵,如過小于corePoolSize則直接創(chuàng)建worker線程
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        //到這個if說明worker線程數(shù)量大于了corePoolSize了映穗,這里直接添加到任務隊列
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        //到這個if說明任務加入隊列失敗,隊列滿了幕随,則再創(chuàng)建線程worker線程蚁滋,如果創(chuàng)建失敗則執(zhí)行拒絕策略
        else if (!addWorker(command, false))
            reject(command);
    }

看到這里從宏觀上我們就能看到整個task投遞到線程池的一個過程,其實這里最主要的方法是addWorker,其實addworker里才是線程池的精華辕录,里面有如何創(chuàng)建線程及start的邏輯澄阳,如何回收過期的線程,如何重復利用創(chuàng)建的線程去運行task的邏輯

4踏拜、線程池是什么時候創(chuàng)建線程的

線程池的線程不是線程池創(chuàng)建的時候創(chuàng)建的碎赢,線程池的線程是在調(diào)用addWorker方法,并且addWorker執(zhí)行成功才會創(chuàng)建
下面我們一起分析一下addworker代碼速梗,這個方法很長肮塞,其實要看明白這個方法我們要先看一下Worker這個類,可以看到這個類實際上實現(xiàn)了Runnable接口姻锁,其實線程池中運行的runnable任務都會被包裝成Worker對象

 private final class Worker
        extends AbstractQueuedSynchronizer
        implements Runnable
    {
     
        private static final long serialVersionUID = 6138294804551838833L;
        //當前worker會綁定一個線程
        final Thread thread;
       
        //當前worker處理的第一個任務
        Runnable firstTask;
        volatile long completedTasks;
        //創(chuàng)建worker時就會生成 一個線程及賦值firstTask枕赵,
        //注意線程池的線程就是在這里被創(chuàng)建的
        Worker(Runnable firstTask) {
            setState(-1); // inhibit interrupts until runWorker
            this.firstTask = firstTask;
            //注意看這里,這個線程創(chuàng)建的時候傳的是this位隶,也就意味著一會this.thread.start時拷窜,
            //執(zhí)行的是worker對象的run方法
            //這個大家想一下,回味一下
            this.thread = getThreadFactory().newThread(this);
        }
}

下面在具體分析一下addWorker是如何start線程及重復利用線程運行任務的

private boolean addWorker(Runnable firstTask, boolean core) {
        retry:
        for (;;) {
            //獲取worker數(shù)量
            int c = ctl.get();
            //獲取線程池狀態(tài)
            int rs = runStateOf(c);

            // 如果線程池狀態(tài)為SHUTDOWN就不接收任務了直接returnfalse
            if (rs >= SHUTDOWN &&
                ! (rs == SHUTDOWN &&
                   firstTask == null &&
                   ! workQueue.isEmpty()))
                return false;

            for (;;) {
                //這里會判斷一下worker數(shù)量
                int wc = workerCountOf(c);
                if (wc >= CAPACITY ||
                    wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
                if (compareAndIncrementWorkerCount(c))
                    break retry;
                c = ctl.get();  // Re-read ctl
                if (runStateOf(c) != rs)
                    continue retry;
                // else CAS failed due to workerCount change; retry inner loop
            }
        }
        
        //如果代碼運行到了這里涧黄,就表示線程池狀態(tài)正常篮昧,任務達到了創(chuàng)建worker線程的條件
        boolean workerStarted = false;
        boolean workerAdded = false;
        Worker w = null;
        try {
            //這里會創(chuàng)建一個worker對象,
            //結合上面的代碼笋妥,worker對象中會包含一個線程和一個firstTask
            w = new Worker(firstTask);
            final Thread t = w.thread;
            if (t != null) {
                //下面這一部分是用來判斷需不需要將worker線程進行緩存懊昨,給其他的任務使用
                final ReentrantLock mainLock = this.mainLock;
                mainLock.lock();
                try {
                    int rs = runStateOf(ctl.get());
                    if (rs < SHUTDOWN ||
                        (rs == SHUTDOWN && firstTask == null)) {
                        if (t.isAlive()) // precheck that t is startable
                            throw new IllegalThreadStateException();
                        //注意看這個workers對象,這是一個hashset春宣,用來緩存創(chuàng)建好的worker對象
                        //注意在強調(diào)一下這個worker對象包含一個Thread引用及Runnable引用
                        workers.add(w);
                        int s = workers.size();
                        if (s > largestPoolSize)
                            largestPoolSize = s;
                        workerAdded = true;
                    }
                } finally {
                    mainLock.unlock();
                }

                //緩存好worker線程后酵颁,這里會執(zhí)行線程的start邏輯
                //注意線程池的任務就是在這里開啟start使任務真正進入runnable狀態(tài)的
                if (workerAdded) {
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
            if (! workerStarted)
                addWorkerFailed(w);
        }
        return workerStarted;
    }

下面我們看一下start的具體邏輯:
剛剛我們已經(jīng)看到了,thread是worker對象的一個屬性月帝,實際上創(chuàng)建線程時躏惋,傳的參數(shù)就是worker對象本身,所以線程start執(zhí)行的邏輯就是worker對象的run方法

//worker的run方法很簡單嚷辅,下面我們看一下runWorker方法
public void run() {
            runWorker(this);
        }

看完這個我們就基本上看完了線程池的核心邏輯了簿姨,但是還有一些細節(jié)沒有仔細看,后面我會提到潦蝇,留給大家思考

final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        //獲取一下fisrtTask
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
        try {
           //這個task什么時候不為空款熬?
           //注意這里會有一個隱含的問題,我們提交的任務存放的順序是 核心worker->隊列->最大線程worker
          //這里的getTask是從隊列里獲取的任務
          //這里task的執(zhí)行順序就變成了攘乒,核心worker->最大線程worker->隊列
          //不知道大家有沒有get到我的點贤牛,可以做一個實驗就是為task編一個號,比如安1-10的順序提交任務则酝,最終執(zhí)行的結果卻是 1殉簸,2闰集,3,4般卑,8武鲁,9,10蝠检,5沐鼠,6,7這種順序叹谁,這里就是出現(xiàn)這種提交順序和執(zhí)行順序不一樣的原理
            while (task != null || (task = getTask()) != null) {
                w.lock();
                if ((runStateAtLeast(ctl.get(), STOP) ||
                     (Thread.interrupted() &&
                      runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                    wt.interrupt();
                try {
                    beforeExecute(wt, task);
                    Throwable thrown = null;
                    try {
                        //這里會最終調(diào)用task的run方法饲梭,到此線程池的創(chuàng)建到運行任務就看結束了
                        task.run();
                    } catch (RuntimeException x) {
                        thrown = x; throw x;
                    } catch (Error x) {
                        thrown = x; throw x;
                    } catch (Throwable x) {
                        thrown = x; throw new Error(x);
                    } finally {
                        afterExecute(task, thrown);
                    }
                } finally {
                    task = null;
                    w.completedTasks++;
                    w.unlock();
                }
            }
            completedAbruptly = false;
        } finally {

            //這里會銷毀線程,在隊列為空的時候焰檩,會銷毀線程憔涉,讓線程數(shù)量停留在corePoolsize范圍內(nèi),什么時候會執(zhí)行到這析苫,當task為空的時候兜叨,什么時候task為空,看getTask的邏輯衩侥,getTask會判斷隊列是否為空及活躍線程的時間來返回task的值国旷,這里就不細講了
            processWorkerExit(w, completedAbruptly);
        }
    }

5、線程池是如何重復利用線程的

看完上面的分析顿乒,其實就能回答這個問題了议街,當任務提交時會創(chuàng)建worker對象,這個對象里會有綁定一個線程璧榄,同時會將worker對象放入workers Set中,創(chuàng)建成功后吧雹,會立馬調(diào)用worker.thread.start方法啟動線程骨杂,這個線程的run方法進行了包裝,首先判斷fisrtTask是否為空如果不為空則直接運行雄卷,否則會while循環(huán)拿緩存隊列中的任務搓蚪,知道緩存隊列為空,或者空閑線程超過了keepalive時間就會銷毀線程丁鹉,以保證線程維持在corePoolSize的大小

6妒潭、任務提交的順序和執(zhí)行的順序是一樣的嗎?

不一樣揣钦,提交順序是 核心worker線程->隊列->非核心worker線程雳灾,
但是執(zhí)行的順序卻是核心worker任務->非核心worker任務->隊列任務
如果能理解這個,線程池就真的理解的差不多了

7冯凹、其他

上面幾個問題有些是我在看源碼時的困惑點谎亩,有些是我看完源碼之后的一些想法,除了這些問題外,線程池還有很多精妙的地方比如匈庭,
a夫凸、線程池的狀態(tài)和核心線程數(shù)其實是用一個4個字節(jié)int表示的,為什么要這么表示
b阱持、線程池中使用到的設計模式有哪些
c夭拌、線程池本身就是并發(fā)場景下提交任務的,那它自己的安全性是如何保證的衷咽,execute方法是如何保證安全性的

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末鸽扁,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子兵罢,更是在濱河造成了極大的恐慌献烦,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,383評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件卖词,死亡現(xiàn)場離奇詭異巩那,居然都是意外死亡,警方通過查閱死者的電腦和手機此蜈,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評論 3 385
  • 文/潘曉璐 我一進店門即横,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人裆赵,你說我怎么就攤上這事东囚。” “怎么了战授?”我有些...
    開封第一講書人閱讀 157,852評論 0 348
  • 文/不壞的土叔 我叫張陵页藻,是天一觀的道長。 經(jīng)常有香客問我植兰,道長份帐,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,621評論 1 284
  • 正文 為了忘掉前任楣导,我火速辦了婚禮废境,結果婚禮上,老公的妹妹穿的比我還像新娘筒繁。我一直安慰自己噩凹,他們只是感情好,可當我...
    茶點故事閱讀 65,741評論 6 386
  • 文/花漫 我一把揭開白布毡咏。 她就那樣靜靜地躺著驮宴,像睡著了一般。 火紅的嫁衣襯著肌膚如雪血当。 梳的紋絲不亂的頭發(fā)上幻赚,一...
    開封第一講書人閱讀 49,929評論 1 290
  • 那天禀忆,我揣著相機與錄音,去河邊找鬼落恼。 笑死箩退,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的佳谦。 我是一名探鬼主播戴涝,決...
    沈念sama閱讀 39,076評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼钻蔑!你這毒婦竟也來了啥刻?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,803評論 0 268
  • 序言:老撾萬榮一對情侶失蹤咪笑,失蹤者是張志新(化名)和其女友劉穎可帽,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體窗怒,經(jīng)...
    沈念sama閱讀 44,265評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡映跟,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,582評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了扬虚。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片努隙。...
    茶點故事閱讀 38,716評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖辜昵,靈堂內(nèi)的尸體忽然破棺而出荸镊,到底是詐尸還是另有隱情,我是刑警寧澤堪置,帶...
    沈念sama閱讀 34,395評論 4 333
  • 正文 年R本政府宣布躬存,位于F島的核電站,受9級特大地震影響舀锨,放射性物質(zhì)發(fā)生泄漏优构。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 40,039評論 3 316
  • 文/蒙蒙 一雁竞、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧拧额,春花似錦碑诉、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至恭垦,卻和暖如春快毛,著一層夾襖步出監(jiān)牢的瞬間格嗅,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,027評論 1 266
  • 我被黑心中介騙來泰國打工唠帝, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留屯掖,地道東北人。 一個月前我還...
    沈念sama閱讀 46,488評論 2 361
  • 正文 我出身青樓襟衰,卻偏偏與公主長得像贴铜,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子瀑晒,可洞房花燭夜當晚...
    茶點故事閱讀 43,612評論 2 350

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