成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

線程池沒你想的那么簡單(續(xù))

svtter / 2870人閱讀

摘要:前言前段時間寫過一篇線程池沒你想的那么簡單,和大家一起擼了一個基本的線程池,具備線程池基本調(diào)度功能。線程池自動擴(kuò)容縮容。回調(diào)以上就是線程池的構(gòu)造函數(shù)以及接口的定義。所以我們在使用線程池時,其中的任務(wù)一定要做好異常處理。線程異常捕獲的重要性。

前言

前段時間寫過一篇《線程池沒你想的那么簡單》,和大家一起擼了一個基本的線程池,具備:

線程池基本調(diào)度功能。

線程池自動擴(kuò)容縮容。

隊(duì)列緩存線程。

關(guān)閉線程池。

這些功能,最后也留下了三個待實(shí)現(xiàn)的 features 。

執(zhí)行帶有返回值的線程。

異常處理怎么辦?

所有任務(wù)執(zhí)行完怎么通知我?

這次就實(shí)現(xiàn)這三個特性來看看 j.u.c 中的線程池是如何實(shí)現(xiàn)這些需求的。

再看本文之前,強(qiáng)烈建議先查看上文《線程池沒你想的那么簡單》
任務(wù)完成后的通知

大家在用線程池的時候或多或少都會有這樣的需求:

線程池中的任務(wù)執(zhí)行完畢后再通知主線程做其他事情,比如一批任務(wù)都執(zhí)行完畢后再執(zhí)行下一波任務(wù)等等。

以我們之前的代碼為例:

總共往線程池中提交了 13 個任務(wù),直到他們都執(zhí)行完畢后再打印 “任務(wù)執(zhí)行完畢” 這個日志。

執(zhí)行結(jié)果如下:

為了簡單的達(dá)到這個效果,我們可以在初始化線程池的時候傳入一個接口的實(shí)現(xiàn),這個接口就是用于任務(wù)完成之后的回調(diào)。

public interface Notify {

    /**
     * 回調(diào)
     */
    void notifyListen() ;
}

以上就是線程池的構(gòu)造函數(shù)以及接口的定義。

所以想要實(shí)現(xiàn)這個功能的關(guān)鍵是在何時回調(diào)這個接口?

仔細(xì)想想其實(shí)也簡單:只要我們記錄提交到線程池中的任務(wù)及完成的數(shù)量,他們兩者的差為 0 時就認(rèn)為線程池中的任務(wù)已執(zhí)行完畢;這時便可回調(diào)這個接口。

所以在往線程池中寫入任務(wù)時我們需要記錄任務(wù)數(shù)量:

為了并發(fā)安全的考慮,這里的計數(shù)器采用了原子的 AtomicInteger 。

而在任務(wù)執(zhí)行完畢后就將計數(shù)器 -1 ,一旦為 0 時則任務(wù)任務(wù)全部執(zhí)行完畢;這時便可回調(diào)我們自定義的接口完成通知。

JDK 的實(shí)現(xiàn)

這樣的需求在 jdk 中的 ThreadPoolExecutor 中也有相關(guān)的 API ,只是用法不太一樣,但本質(zhì)原理都大同小異。

我們使用 ThreadPoolExecutor 的常規(guī)關(guān)閉流程如下:

    executorService.shutdown();
    while (!executorService.awaitTermination(100, TimeUnit.MILLISECONDS)) {
        logger.info("thread running");
    }

線程提交完畢后執(zhí)行 shutdown() 關(guān)閉線程池,接著循環(huán)調(diào)用 awaitTermination() 方法,一旦任務(wù)全部執(zhí)行完畢后則會返回 true 從而退出循環(huán)。

這兩個方法的目的和原理如下:

執(zhí)行 shutdown() 后會將線程池的狀態(tài)置為關(guān)閉狀態(tài),這時將會停止接收新的任務(wù)同時會等待隊(duì)列中的任務(wù)全部執(zhí)行完畢后才真正關(guān)閉線程池。

awaitTermination 會阻塞直到線程池所有任務(wù)執(zhí)行完畢或者超時時間已到。

為什么要兩個 api 結(jié)合一起使用呢?

主要還在最終的目的是:所有線程執(zhí)行完畢后再做某件事情,也就是在線程執(zhí)行完畢之前其實(shí)主線程是需要被阻塞的。

shutdown() 執(zhí)行后并不會阻塞,會立即返回,所有才需要后續(xù)用循環(huán)不停的調(diào)用 awaitTermination(),因?yàn)檫@個 api 才會阻塞線程。

其實(shí)我們查看源碼會發(fā)現(xiàn),ThreadPoolExecutor 中的阻塞依然也是等待通知機(jī)制的運(yùn)用,只不過用的是 LockSupportAPI 而已。

帶有返回值的線程

接下來是帶有返回值的線程,這個需求也非常常見;比如需要線程異步計算某些數(shù)據(jù)然后得到結(jié)果最終匯總使用。

先來看看如何使用(和 jdk 的類似):

首先任務(wù)是不能實(shí)現(xiàn) Runnable 接口了,畢竟他的 run() 函數(shù)是沒有返回值的;所以我們改實(shí)現(xiàn)一個 Callable 的接口:

這個接口有一個返回值。

同時在提交任務(wù)時也稍作改動:

首先是執(zhí)行任務(wù)的函數(shù)由 execute() 換為了 submit(),同時他會返回一個返回值 Future,通過它便可拿到線程執(zhí)行的結(jié)果。

最后通過第二步將所有執(zhí)行結(jié)果打印出來:

實(shí)現(xiàn)原理

再看具體實(shí)現(xiàn)之前先來思考下這樣的功能如何實(shí)現(xiàn)?

首先受限于 jdk 的線程 api 的規(guī)范,要執(zhí)行一個線程不管是實(shí)現(xiàn)接口還是繼承類,最終都是執(zhí)行的 run() 函數(shù)。

所以我們想要一個線程有返回值無非只能是在執(zhí)行 run() 函數(shù)時去調(diào)用一個有返回值的方法,再將這個返回值存放起來用于后續(xù)使用。

比如我們這里新建了一個 Callable 的接口:

public interface Callable {

    /**
     * 執(zhí)行任務(wù)
     * @return 執(zhí)行結(jié)果
     */
    T call() ;
}

它的 call 函數(shù)就是剛才提到的有返回值的方法,所以我們應(yīng)當(dāng)在線程的 run() 函數(shù)中去調(diào)用它。

接著還會有一個 Future 的接口,他的主要作用是獲取線程的返回值,也就是 再將這個返回值存放起來用于后續(xù)使用 這里提到的后續(xù)使用。

既然有了接口那自然就得有它的實(shí)現(xiàn) FutureTask,它實(shí)現(xiàn)了 Future 接口用于后續(xù)獲取返回值。

同時實(shí)現(xiàn)了 Runnable 接口會把自己變?yōu)橐粋€線程。

所以在它的 run() 函數(shù)中會調(diào)用剛才提到的具有返回值的 call() 函數(shù)。

再次結(jié)合 submit() 提交任務(wù)和 get() 獲取返回值的源碼來看會更加理解這其中的門道。

    /**
     * 有返回值
     *
     * @param callable
     * @param 
     * @return
     */
    public  Future submit(Callable callable) {
        FutureTask future = new FutureTask(callable);
        execute(future);
        return future;
    }

submit() 非常簡單,將我們丟進(jìn)來的 Callable 對象轉(zhuǎn)換為一個 FutureTask 對象,然后再調(diào)用之前的 execute() 來丟進(jìn)線程池(后續(xù)的流程就和一個普通的線程進(jìn)入線程池的流程一樣)。

FutureTask 本身也是線程,所以可以直接使用 execute() 函數(shù)。

future.get() 函數(shù)中 future 對象由于在 submit() 中返回的真正對象是 FutureTask,所以我們直接看其中的源碼就好。

由于 get() 在線程沒有返回之前是一個阻塞函數(shù),最終也是通過 notify.wait() 使線程進(jìn)入阻塞狀態(tài)來實(shí)現(xiàn)的。

而使其從 wait() 中返回的條件必然是在線程執(zhí)行完畢拿到返回值的時候才進(jìn)行喚醒。

也就是圖中的第二部分;一旦線程執(zhí)行完畢(callable.call())就會喚醒 notify 對象,這樣 get 方法也就能返回了。

同樣的道理,ThreadPoolExecutor 中的原理也是類似,只不過它考慮的細(xì)節(jié)更多所以看起來很復(fù)雜,但精簡代碼后核心也就是這些。

甚至最終使用的 api 看起來都是類似的:

異常處理

最后一個是一些新手使用線程池很容易踩坑的一個地方:那就是異常處理。

比如類似于這樣的場景:

創(chuàng)建了只有一個線程的線程池,這個線程只做一件事,就是一直不停的 while 循環(huán)。

但是循環(huán)的過程中不小心拋出了一個異常,巧的是這個異常又沒有被捕獲。你覺得后續(xù)會發(fā)生什么事情呢?

是線程繼續(xù)運(yùn)行?還是線程池會退出?

通過現(xiàn)象來看其實(shí)哪種都不是,線程既沒有繼續(xù)運(yùn)行同時線程池也沒有退出,會一直卡在這里。

當(dāng)我們 dump 線程快照會發(fā)現(xiàn):

這時線程池中還有一個線程在運(yùn)行,通過線程名稱會發(fā)現(xiàn)這是新創(chuàng)建的一個線程(之前是Thread-0,現(xiàn)在是 Thread-1)。

它的線程狀態(tài)為 WAITING ,通過堆棧發(fā)現(xiàn)是卡在了 CustomThreadPool.java:272 處。

就是卡在了從隊(duì)列里獲取任務(wù)的地方,由于此時的任務(wù)隊(duì)列是空的,所以他會一直阻塞在這里。

看到這里,之前關(guān)注的朋友有沒有似曾相識的感覺。

沒錯,我之前寫過兩篇:

一個線程罷工的詭異事件

線程池中你不容錯過的一些細(xì)節(jié)

線程池相關(guān)的問題,當(dāng)時的討論也非常“激烈”,其實(shí)最終的原因和這里是一模一樣的。

所以就這次簡版的代碼來看看其中的問題:

現(xiàn)在又簡化了一版代碼我覺得之前還有疑問的朋友這次應(yīng)該會更加明白。

其實(shí)在線程池內(nèi)部會對線程的運(yùn)行捕獲異常,但它并不會處理,只是用于標(biāo)記是否執(zhí)行成功;

一旦執(zhí)行失敗則會回收掉當(dāng)前異常的線程,然后重新創(chuàng)建一個新的 Worker 線程繼續(xù)從隊(duì)列里取任務(wù)然后執(zhí)行。

所以最終才會卡在從隊(duì)列中取任務(wù)處。

其實(shí) ThreadPoolExecutor 的異常處理也是類似的,具體的源碼就不多分析了,在上面兩篇文章中已經(jīng)說過幾次。

所以我們在使用線程池時,其中的任務(wù)一定要做好異常處理。

總結(jié)

這一波下來我覺得線程池搞清楚沒啥問題了,總的來看它內(nèi)部運(yùn)用了非常多的多線程解決方案,比如:

ReentrantLock 重入鎖來保證線程寫入的并發(fā)安全。

利用等待通知機(jī)制來實(shí)現(xiàn)線程間通信(線程執(zhí)行結(jié)果、等待線程池執(zhí)行完畢等)。

最后也學(xué)會了:

標(biāo)準(zhǔn)的線程池關(guān)閉流程。

如何使用有返回值的線程。

線程異常捕獲的重要性。

最后本文所有源碼(結(jié)合其中的測試代碼使用):

https://github.com/crossoverJie/JCSprout/blob/master/src/main/java/com/crossoverjie/concurrent/CustomThreadPool.java

你的點(diǎn)贊與分享是對我最大的支持

文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/74824.html

相關(guān)文章

  • 線程池沒想的那么簡單

    摘要:如何優(yōu)雅的使用和理解線程池線程池中你不容錯過的一些細(xì)節(jié)由于篇幅限制,本次可能會分為上下兩篇。不接受新的任務(wù),同時等待現(xiàn)有任務(wù)執(zhí)行完畢后退出線程池。慎用方法關(guān)閉線程池,會導(dǎo)致任務(wù)丟失除非業(yè)務(wù)允許。前言 原以為線程池還挺簡單的(平時常用,也分析過原理),這次是想自己動手寫一個線程池來更加深入的了解它;但在動手寫的過程中落地到細(xì)節(jié)時發(fā)現(xiàn)并沒想的那么容易。結(jié)合源碼對比后確實(shí)不得不佩服 Doug Le...

    Leck1e 評論0 收藏0
  • 線程池沒想的那么簡單

    摘要:如何優(yōu)雅的使用和理解線程池線程池中你不容錯過的一些細(xì)節(jié)由于篇幅限制,本次可能會分為上下兩篇。不接受新的任務(wù),同時等待現(xiàn)有任務(wù)執(zhí)行完畢后退出線程池。慎用方法關(guān)閉線程池,會導(dǎo)致任務(wù)丟失除非業(yè)務(wù)允許。 showImg(https://segmentfault.com/img/remote/1460000019230693); 前言 原以為線程池還挺簡單的(平時常用,也分析過原理),這次是想自...

    ruicbAndroid 評論0 收藏0
  • 學(xué)習(xí)python12小時后,告訴你,學(xué)python真沒想的那么難!

    摘要:列入全國計算機(jī)二級取代,部分城市試點(diǎn),引入高中。建議通過視頻學(xué)習(xí),這樣不但節(jié)省時間,而且效果很好。能否回憶起那個陡峭的學(xué)習(xí)曲線問題越多,學(xué)的越快。出報告每完成一個項(xiàng)目,總結(jié)報告,必不可少。結(jié)構(gòu)化學(xué)習(xí),才是你我需要真正培養(yǎng)的能力。 編程就如同你學(xué)習(xí)開車,即使,你可以一口氣,說出一輛車的全部零部件,以及內(nèi)燃機(jī)進(jìn)氣、壓縮、做功和排氣過程,但你就是不去練如何開車,怎么上路。你確定,你敢開嗎?你...

    Kaede 評論0 收藏0
  • 通過 React Hooks 聲明式地使用 setInterval

    摘要:但我認(rèn)為談不上的毛病,而是編程模型和之間的一種模式差異。相比類,更貼近編程模型,使得這種差異更加突出。聲明本文采用循序漸進(jìn)的示例來解釋問題。本文假設(shè)讀者已經(jīng)使用超過一個小時。這是通過組件生命周期上綁定與的組合完成的。 本文由云+社區(qū)發(fā)表作者:Dan Abramov 接觸 React Hooks 一定時間的你,也許會碰到一個神奇的問題: setInterval 用起來沒你想的簡單。 R...

    NoraXie 評論0 收藏0

發(fā)表評論

0條評論

最新活動
閱讀需要支付1元查看
<