線程池

線程池

線程是寶貴的內(nèi)存資源枢希、單個(gè)線程約占1MB空間,過(guò)多分配易造成內(nèi)存溢出

頻繁的創(chuàng)建及銷(xiāo)毀線程會(huì)增加虛擬機(jī)回收頻率劳淆、資源開(kāi)銷(xiāo)兰伤,造成程序性能下降

線程池是線程的容器∧砑ぃ可設(shè)定線程分配的數(shù)量上限噪矛,將預(yù)先創(chuàng)建的線程對(duì)象存入池中,并重用線程池中的線程對(duì)象铺罢,避免線程頻繁的創(chuàng)建和銷(xiāo)毀艇挨。

一、JDK提供創(chuàng)建線程池的四種快捷方式

//創(chuàng)建一個(gè)單線程化的線程池韭赘,它只會(huì)用唯一的工作線程來(lái)執(zhí)行任務(wù)缩滨,保證所有任務(wù)按照指定順序(FIFO, LIFO, 優(yōu)先級(jí))執(zhí)行。
//適用于需要保證順序執(zhí)行各個(gè)任務(wù)泉瞻。
ExecutorService pool01 = Executors.newSingleThreadExecutor();

//創(chuàng)建一個(gè)定長(zhǎng)線程池脉漏,可控制線程最大并發(fā)數(shù),超出的線程會(huì)在隊(duì)列中等待袖牙。因?yàn)椴捎脽o(wú)界的阻塞隊(duì)列侧巨,所以實(shí)際線程數(shù)量永遠(yuǎn)不會(huì)變化。
//適用于負(fù)載較重的場(chǎng)景鞭达,對(duì)當(dāng)前線程數(shù)量進(jìn)行限制司忱。(保證線程數(shù)可控皇忿,不會(huì)造成線程過(guò)多,導(dǎo)致系統(tǒng)負(fù)載更為嚴(yán)重)
ExecutorService pool02 = Executors.newFixedThreadPool(4);

//創(chuàng)建一個(gè)可緩存線程池坦仍,如果線程池長(zhǎng)度超過(guò)處理需要鳍烁,可靈活回收空閑線程,若無(wú)可回收繁扎,則新建線程幔荒。
//適用于負(fù)載較輕的場(chǎng)景,執(zhí)行短期異步任務(wù)梳玫。(可以使得任務(wù)快速得到執(zhí)行爹梁,因?yàn)槿蝿?wù)時(shí)間執(zhí)行短,可以很快結(jié)束提澎,也不會(huì)造成cpu過(guò)度切換)
ExecutorService pool03 = Executors.newCachedThreadPool();

//創(chuàng)建一個(gè)定長(zhǎng)線程池姚垃,支持定時(shí)及周期性任務(wù)執(zhí)行。
//適用于執(zhí)行延時(shí)或者周期性任務(wù)虱朵。
ScheduledExecutorService pool04 = Executors.newScheduledThreadPool(4);

二莉炉、工作原理

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
   
}

int corePoolSize:核心線程數(shù)

  • 核心線程會(huì)一直存活钓账,即使沒(méi)有任務(wù)需要執(zhí)行碴犬。

  • 當(dāng)線程數(shù)小于核心線程數(shù)時(shí),即使有線程空閑梆暮,線程池也會(huì)優(yōu)先創(chuàng)建新線程處理服协。

  • 設(shè)置 allowCoreThreadTimeout=true(默認(rèn)false)時(shí),核心線程會(huì)超時(shí)關(guān)閉啦粹。

  • 可以通過(guò) prestartCoreThread() 或 prestartAllCoreThreads() 方法來(lái)提前啟動(dòng)線程池中的基本線程偿荷。

int maxPoolSize:最大線程數(shù)

  • 線程池所允許的最大線程個(gè)數(shù)

  • maxPoolSize>當(dāng)線程數(shù)>=corePoolSize,且任務(wù)隊(duì)列已滿時(shí)唠椭。線程池會(huì)創(chuàng)建新線程來(lái)處理任務(wù)跳纳。

  • 當(dāng)線程數(shù)=maxPoolSize,且任務(wù)隊(duì)列已滿時(shí)贪嫂,線程池會(huì)根據(jù)handle策略處理寺庄,默認(rèn)是AbortPolicy 丟棄任務(wù),拋運(yùn)行時(shí)異常力崇。

long keepAliveTime:非核心線程空閑保持時(shí)間

  • 當(dāng)線程空閑時(shí)間達(dá)到 keepAliveTime 時(shí)斗塘,線程會(huì)退出,直到線程數(shù)量= corePoolSize亮靴。

  • 如果 allowCoreThreadTimeout = true馍盟,則會(huì)直到線程數(shù)量=0。

TimeUnit unit:時(shí)間單位

TimeUnit是一個(gè)枚舉類(lèi)型 茧吊,包括以下屬性:

NANOSECONDS : 1微毫秒 = 1微秒 / 1000 MICROSECONDS : 1微秒 = 1毫秒 / 1000 MILLISECONDS : 1毫秒 = 1秒 /1000 SECONDS : 秒 MINUTES : 分 HOURS : 小時(shí) DAYS : 天

BlockingQueue workQueue:任務(wù)隊(duì)列容量(阻塞隊(duì)列)

  • 當(dāng)核心線程數(shù)達(dá)到最大時(shí)贞岭,新任務(wù)會(huì)放在隊(duì)列中排隊(duì)等待執(zhí)行八毯。

  • 常用的幾個(gè)阻塞隊(duì)列:

    • LinkedBlockingQueue

      鏈?zhǔn)阶枞?duì)列,底層數(shù)據(jù)結(jié)構(gòu)是鏈表曹步,默認(rèn)大小是Integer.MAX_VALUE宪彩,也可以指定大小。

    • ArrayBlockingQueue

      數(shù)組阻塞隊(duì)列讲婚,底層數(shù)據(jù)結(jié)構(gòu)是數(shù)組尿孔,需要指定隊(duì)列的大小。

    • SynchronousQueue

      同步隊(duì)列筹麸,內(nèi)部容量為0活合,每個(gè)put操作必須等待一個(gè)take操作,反之亦然物赶。

    • DelayQueue

      延遲隊(duì)列白指,該隊(duì)列中的元素只有當(dāng)其指定的延遲時(shí)間到了,才能夠從隊(duì)列中獲取到該元素 酵紫。

ThreadFactory threadFactory:線程工廠

用于批量創(chuàng)建線程告嘲,統(tǒng)一在創(chuàng)建線程時(shí)設(shè)置一些參數(shù),如是否守護(hù)線程奖地、線程的優(yōu)先級(jí)等橄唬。如果不指定,會(huì)新建一個(gè)默認(rèn)的線程工廠参歹。

RejectedExecutionHandler handler:任務(wù)拒絕處理器

  • 兩種情況會(huì)拒絕處理任務(wù):

    • 當(dāng)線程數(shù)已經(jīng)達(dá)到 maxPoolSize仰楚,切隊(duì)列已滿,會(huì)拒絕新任務(wù)犬庇。
    • 當(dāng)線程池被調(diào)用 shutdown() 后僧界,會(huì)等待線程池里的任務(wù)執(zhí)行完畢,再 shutdown臭挽。如果在調(diào)用shutdown() 和線程池真正 shutdown 之間提交任務(wù)捂襟,會(huì)拒絕新任務(wù)。線程池會(huì)調(diào)用rejectedExecutionHandler 來(lái)處理這個(gè)任務(wù)欢峰。如果沒(méi)有設(shè)置默認(rèn)是 AbortPolicy葬荷,會(huì)拋出異常。
  • ThreadPoolExecutor 類(lèi)有幾個(gè)內(nèi)部實(shí)現(xiàn)類(lèi)來(lái)處理這類(lèi)情況-handle飽和策略:

    • ThreadPoolExecutor.AbortPolicy默認(rèn)拒絕處理策略赤赊,丟棄任務(wù)并拋出RejectedExecutionException異常闯狱。
    • ThreadPoolExecutor.DiscardPolicy:丟棄新來(lái)的任務(wù),但是不拋出異常抛计。
    • ThreadPoolExecutor.DiscardOldestPolicy:丟棄隊(duì)列頭部(最舊的)的任務(wù)哄孤,然后重新嘗試執(zhí)行程序(如果再次失敗,重復(fù)此過(guò)程)吹截。
    • ThreadPoolExecutor.CallerRunsPolicy:由調(diào)用線程處理該任務(wù)瘦陈。

三凝危、線程池使用不當(dāng)?shù)奈:?/h2>

不知道大家在使用線程池的時(shí)候,是否存在以下的疑惑晨逝,這是我看到的某些同事的代碼蛾默,他們是這樣使用線程池的:

1、 線程池局部變量捉貌,new出來(lái)使用支鸡,沒(méi)有手動(dòng)shutdown
2、 線程池局部變量趁窃,new出來(lái)使用牧挣,并且最后手動(dòng)shutdown
3、 線程池定義為static類(lèi)型醒陆,進(jìn)行類(lèi)復(fù)用

大家先想想到底哪種方式是正確的瀑构,以及錯(cuò)誤的方式可能會(huì)帶來(lái)什么問(wèn)題

方式一:線程池局部使用,沒(méi)有shutdown

首先刨摩,我們明確:局部變量new出來(lái)的線程池寺晌,執(zhí)行這段代碼的程序的每一個(gè)線程都會(huì)去創(chuàng)建一個(gè)局部的線程池。暫且不說(shuō)每一個(gè)線程都去創(chuàng)建線程池是出于什么神奇的目的澡刹,首當(dāng)其沖的線程池的復(fù)用的性質(zhì)就被打破了呻征。創(chuàng)建出來(lái)的線程池都得不到復(fù)用,那么還有什么必要花費(fèi)大精力創(chuàng)建線程池像屋?

所以線程池局部使用本身就是不推薦的使用方式怕犁!

其次边篮,我們?cè)賮?lái)想想己莺,局部使用線程池,同時(shí)設(shè)置核心線程不為0戈轿,且設(shè)置allowCoreThreadTimeOut=false(空閑后不回收核心線程池)會(huì)導(dǎo)致什么問(wèn)題凌受?(想都不用想,核心線程池得不到回收思杯,自然會(huì)導(dǎo)致OOM)

以下是問(wèn)題代碼:

public static void main(String[] args) {
    while (true) {
        try {
            //newFixedThreadPool不會(huì)回收核心線程 可能導(dǎo)致OOM
            ExecutorService service = Executors.newFixedThreadPool(1);
            service.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(2000); 模擬處理業(yè)務(wù)
                    } catch (InterruptedException e) {
                    }
                }
            });
            service = null;
        } catch (Exception e) {
        }
        try {
            Thread.sleep(2000);
            System.gc();
        } catch (InterruptedException e) {
        }
    }
}

那么胜蛉,是否我們?cè)O(shè)置核心線程可以回收不就好咯?同樣會(huì)出現(xiàn)問(wèn)題色乾。系統(tǒng)可能會(huì)根據(jù)你設(shè)置的線程過(guò)期時(shí)間誊册,呈現(xiàn)有規(guī)律的內(nèi)存占用上升,然后下降暖璧,然后又上升案怯,然后又下降的趨勢(shì)。你說(shuō)說(shuō)這是好的內(nèi)存運(yùn)行情況澎办?

方式二:線程池局部使用嘲碱,使用完后手動(dòng)shutdown線程池

okok金砍,這種方式OOM的風(fēng)險(xiǎn)降低了,但是又是局部使用局部使用麦锯,你干嘛要局部使用線程池呢恕稠?這樣不就使得每一個(gè)線程都會(huì)new一個(gè)線程池,導(dǎo)致線程池不會(huì)復(fù)用扶欣,這和你不用線程池有什么區(qū)別呢鹅巍?系統(tǒng)還白白花費(fèi)資源去創(chuàng)建線程池。

方式三:線程池定義為static類(lèi)型料祠,進(jìn)行類(lèi)復(fù)用

明顯昆著,到這里才是正確的使用線程池的方式。static修飾的類(lèi)變量只會(huì)加載一次术陶,所有的線程共享這一個(gè)線程池了唄凑懂。以下是正確的使用代碼:

public class staticTestExample {
    //static 定義線程池
    private static ThreadPoolExecutor pool = new ThreadPoolExecutor(
            corePoolSize,
            maximumPoolSize,
            timeout,
            TimeUnit.SECONDS, 
            new LinkedBlockingDeque<>(), 
            new ThreadPoolExecutor.AbortPolicy());
   
    public static void main(String[] args) {
        //使用線程池
        Future<Boolean> submit = pool.submit(() -> true);
    }
}

業(yè)務(wù)中的線程池的復(fù)用

這里給出以下兩種思路

第一種:根據(jù)業(yè)務(wù)執(zhí)行類(lèi)型去創(chuàng)建線程池(同一類(lèi)型的業(yè)務(wù)復(fù)用一個(gè)線程池)
??簡(jiǎn)單來(lái)說(shuō),業(yè)務(wù)場(chǎng)景相同梧宫,且需要用到線程池的地方接谨,復(fù)用一個(gè)線程池。比如塘匣,拆分任務(wù)場(chǎng)景脓豪,一次性需要同時(shí)拆分100個(gè)任務(wù)去執(zhí)行,就可以把這100個(gè)相同業(yè)務(wù)場(chǎng)景的任務(wù)交給一個(gè)特定命名的線程池處理忌卤。這個(gè)線程池就是專(zhuān)門(mén)去處理任務(wù)拆分的扫夜。

代碼如下:

public class ThreadPoolUtil {
    private static final Map<String, ExecutorService> POOL_CATCH = Maps.newConcurrentMap();
    /**
     * 根據(jù)特定名稱(chēng)去緩存線程池
     * @param poolName
     * @return
     */
    public static ExecutorService create(String poolName) {
        //map有緩存直接復(fù)用緩存中存在的線程池
        if (POOL_CATCH.containsKey(poolName)) {
            return POOL_CATCH.get(poolName);
        }
        // synchronized鎖字符串常量池字段 防止并發(fā),使得map中緩存的線程池驰徊,只會(huì)創(chuàng)建一次
        synchronized (poolName.intern()) {
            int poolSize = Runtime.getRuntime().availableProcessors() * 2;
            ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(poolSize, poolSize,
                    30L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(),new ThreadPoolExecutor.AbortPolicy());
            POOL_CATCH.put(poolName, threadPoolExecutor);
            return threadPoolExecutor;
        }
    }
}
public class CorrectTest {
    public static void main(String[] args) {
        ExecutorService pool = ThreadPoolUtil.create("拆分任務(wù)線程池");
        //線程池執(zhí)行任務(wù)
        Future<Boolean> submit = pool.submit(() -> true);
    }
}

第二種:根據(jù)用戶登陸性質(zhì)去創(chuàng)建線程池(用一類(lèi)型的用戶復(fù)用一個(gè)線程池)
??簡(jiǎn)單來(lái)說(shuō)笤闯,用戶類(lèi)型且業(yè)務(wù)場(chǎng)景相同,需要用到線程池的地方棍厂,復(fù)用一個(gè)線程池颗味。

public class CorrectTest {
    public static void main(String[] args) {
        //模擬得到用戶信息
        Userinfo  = getUserinfo();
        //模擬用相同的用戶類(lèi)型(type)去創(chuàng)建線程池
        ExecutorService pool = ThreadPoolUtil.create(Userinfo.getType);
        //線程池執(zhí)行任務(wù)
        Future<Boolean> submit = pool.submit(() -> true);
    }
}

總結(jié)

  1. 使用全局線程池而不是局部線程池,否則可能會(huì)有連續(xù)創(chuàng)建局部線程池的OOM風(fēng)險(xiǎn)
  2. 就算使用局部線程池牺弹,最后一定要shutdown浦马,否則可能導(dǎo)致不回收核心線程的內(nèi)存泄漏
  3. 理解線程池是為了復(fù)用的,不要代碼中隨意new一個(gè)局部線程池

四张漂、結(jié)語(yǔ)

四種常見(jiàn)的線程池基本夠我們使用了晶默,但是《阿里巴巴開(kāi)發(fā)手冊(cè)》不建議我們直接使用Executors類(lèi)中的線程池,而是通過(guò)ThreadPoolExecutor的方式航攒,這樣的處理方式讓寫(xiě)的同學(xué)需要更加明確線程池的運(yùn)行規(guī)則磺陡,規(guī)避資源耗盡的風(fēng)險(xiǎn)。

但如果你及團(tuán)隊(duì)本身對(duì)線程池非常熟悉,又確定業(yè)務(wù)規(guī)模不會(huì)大到資源耗盡的程度(比如線程數(shù)量或任務(wù)隊(duì)列長(zhǎng)度可能達(dá)到Integer.MAX_VALUE)時(shí)仅政,其實(shí)是可以使用JDK提供的這幾個(gè)接口的垢油,它能讓我們的代碼具有更強(qiáng)的可讀性。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末圆丹,一起剝皮案震驚了整個(gè)濱河市滩愁,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌辫封,老刑警劉巖硝枉,帶你破解...
    沈念sama閱讀 211,561評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異倦微,居然都是意外死亡妻味,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,218評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門(mén)欣福,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)责球,“玉大人,你說(shuō)我怎么就攤上這事拓劝〕猓” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 157,162評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵郑临,是天一觀的道長(zhǎng)栖博。 經(jīng)常有香客問(wèn)我,道長(zhǎng)厢洞,這世上最難降的妖魔是什么仇让? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,470評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮躺翻,結(jié)果婚禮上丧叽,老公的妹妹穿的比我還像新娘。我一直安慰自己获枝,他們只是感情好蠢正,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,550評(píng)論 6 385
  • 文/花漫 我一把揭開(kāi)白布骇笔。 她就那樣靜靜地躺著省店,像睡著了一般。 火紅的嫁衣襯著肌膚如雪笨触。 梳的紋絲不亂的頭發(fā)上懦傍,一...
    開(kāi)封第一講書(shū)人閱讀 49,806評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音芦劣,去河邊找鬼粗俱。 笑死,一個(gè)胖子當(dāng)著我的面吹牛虚吟,可吹牛的內(nèi)容都是我干的寸认。 我是一名探鬼主播签财,決...
    沈念sama閱讀 38,951評(píng)論 3 407
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼偏塞!你這毒婦竟也來(lái)了唱蒸?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,712評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤灸叼,失蹤者是張志新(化名)和其女友劉穎神汹,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體古今,經(jīng)...
    沈念sama閱讀 44,166評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡屁魏,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,510評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了捉腥。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片氓拼。...
    茶點(diǎn)故事閱讀 38,643評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖抵碟,靈堂內(nèi)的尸體忽然破棺而出披诗,到底是詐尸還是另有隱情,我是刑警寧澤立磁,帶...
    沈念sama閱讀 34,306評(píng)論 4 330
  • 正文 年R本政府宣布呈队,位于F島的核電站,受9級(jí)特大地震影響唱歧,放射性物質(zhì)發(fā)生泄漏宪摧。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,930評(píng)論 3 313
  • 文/蒙蒙 一颅崩、第九天 我趴在偏房一處隱蔽的房頂上張望几于。 院中可真熱鬧,春花似錦沿后、人聲如沸沿彭。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,745評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)喉刘。三九已至,卻和暖如春漆弄,著一層夾襖步出監(jiān)牢的瞬間睦裳,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,983評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工撼唾, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留廉邑,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,351評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像蛛蒙,于是被迫代替她去往敵國(guó)和親糙箍。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,509評(píng)論 2 348

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