一份針對于新手的多線程實踐

image

前言

image

前段時間在某個第三方平臺看到我寫作字?jǐn)?shù)居然突破了 10W 字,難以想象高中 800 字作文我都得巧妙的利用換行來完成(懂的人肯定也干過??)。

干了這行養(yǎng)成了一個習(xí)慣:能擼碼驗證的事情都自己驗證一遍辕狰。

于是在上周五通宵加班的空余時間寫了一個工具:

https://github.com/crossoverJie/NOWS

利用 SpringBoot 只需要一行命令即可統(tǒng)計自己寫了多少個字径簿。

java -jar nows-0.0.1-SNAPSHOT.jar /xx/Hexo/source/_posts

傳入需要掃描的文章目錄即可輸出結(jié)果(目前只支持 .md 結(jié)尾 Markdown 文件)

image

當(dāng)然結(jié)果看個樂就行(40 幾萬字)芬位,因為早期的博客我喜歡大篇的貼代碼凿渊,還有一些英文單詞也沒有過濾,所以導(dǎo)致結(jié)果相差較大挫鸽。

如果僅僅只是中文文字統(tǒng)計肯定是準(zhǔn)的说敏,并且該工具內(nèi)置靈活的擴展方式,使用者可以自定義統(tǒng)計策略丢郊,具體請看后文像云。

其實這個工具挺簡單的,代碼量也少蚂夕,沒有多少可以值得拿出來講的迅诬。但經(jīng)過我回憶不管是面試還是和網(wǎng)友們交流都發(fā)現(xiàn)一個普遍的現(xiàn)象:

大部分新手開發(fā)都會去看多線程、但幾乎都沒有相關(guān)的實踐婿牍。甚至有些都不知道多線程拿來在實際開發(fā)中有什么用侈贷。

為此我想基于這個簡單的工具為這類朋友帶來一個可實踐、易理解的多線程案例等脂。

至少可以讓你知道:

  • 為什么需要多線程俏蛮?
  • 怎么實現(xiàn)一個多線程程序?
  • 多線程帶來的問題及解決方案上遥?

單線程統(tǒng)計

再談多線程之前先來聊聊單線程如何實現(xiàn)搏屑。

本次的需求也很簡單,只是需要掃描一個目錄讀取下面的所有文件即可粉楚。

所有我們的實現(xiàn)有以下幾步:

  • 讀取某個目錄下的所有文件辣恋。
  • 將所有文件的路徑保持到內(nèi)存。
  • 遍歷所有的文件挨個讀取文本記錄字?jǐn)?shù)即可模软。

先來看前兩個如何實現(xiàn)伟骨,并且當(dāng)掃描到目錄時需要繼續(xù)讀取當(dāng)前目錄下的文件。

這樣的場景就非常適合遞歸:

    public List<String> getAllFile(String path){

        File f = new File(path) ;
        File[] files = f.listFiles();
        for (File file : files) {
            if (file.isDirectory()){
                String directoryPath = file.getPath();
                getAllFile(directoryPath);
            }else {
                String filePath = file.getPath();
                if (!filePath.endsWith(".md")){
                    continue;
                }
                allFile.add(filePath) ;
            }
        }

        return allFile ;
    }
}

讀取之后將文件的路徑保持到一個集合中燃异。

需要注意的是這個遞歸次數(shù)需要控制下携狭,避免出現(xiàn)棧溢出(StackOverflow)。

最后讀取文件內(nèi)容則是使用 Java8 中的流來進行讀取回俐,這樣代碼可以更簡潔:

Stream<String> stringStream = Files.lines(Paths.get(path), StandardCharsets.UTF_8);
List<String> collect = stringStream.collect(Collectors.toList());

接下來便是讀取字?jǐn)?shù)逛腿,同時要過濾一些特殊文本(比如我想過濾掉所有的空格稀并、換行、超鏈接等)单默。

擴展能力

簡單處理可在上面的代碼中遍歷 collect 然后把其中需要過濾的內(nèi)容替換為空就行碘举。

但每個人的想法可能都不一樣。比如我只想過濾掉空格雕凹、換行殴俱、超鏈接就行了政冻,但有些人需要去掉其中所有的英文單詞枚抵,甚至換行還得留著(就像寫作文一樣可以充字?jǐn)?shù))。

所有這就需要一個比較靈活的處理方式明场。

看過上文《利用責(zé)任鏈模式設(shè)計一個攔截器》應(yīng)該很容易想到這樣的場景責(zé)任鏈模式再合適不過了汽摹。

關(guān)于責(zé)任鏈模式具體的內(nèi)容就不在詳述了,感興趣的可以查看上文苦锨。

這里直接看實現(xiàn)吧:

定義責(zé)任鏈的抽象接口及處理方法:

public interface FilterProcess {
    /**
     * 處理文本
     * @param msg
     * @return
     */
    String process(String msg) ;
}

處理空格和換行的實現(xiàn):

public class WrapFilterProcess implements FilterProcess{
    @Override
    public String process(String msg) {
        msg = msg.replaceAll("\\s*", "");
        return msg ;
    }
}

處理超鏈接的實現(xiàn):

public class HttpFilterProcess implements FilterProcess{
    @Override
    public String process(String msg) {
        msg = msg.replaceAll("^((https|http|ftp|rtsp|mms)?:\\/\\/)[^\\s]+","");
        return msg ;
    }
}

這樣在初始化時需要將這些處理 handle 都加入責(zé)任鏈中逼泣,同時提供一個 API 供客戶端執(zhí)行即可。

image

這樣一個簡單的統(tǒng)計字?jǐn)?shù)的工具就完成了舟舒。

多線程模式

在我本地一共就幾十篇博客的條件下執(zhí)行一次還是很快的拉庶,但如果我們的文件是幾萬、幾十萬甚至上百萬呢秃励。

雖然功能可以實現(xiàn)氏仗,但可以想象這樣的耗時絕對是成倍的增加。

這時多線程就發(fā)揮優(yōu)勢了夺鲜,由多個線程分別去讀取文件最后匯總結(jié)果即可皆尔。

這樣實現(xiàn)的過程就變?yōu)椋?/p>

  • 讀取某個目錄下的所有文件。
  • 將文件路徑交由不同的線程自行處理币励。
  • 最終匯總結(jié)果慷蠕。

多線程帶來的問題

也不是使用多線程就萬事大吉了,先來看看第一個問題:共享資源食呻。

簡單來說就是怎么保證多線程和單線程統(tǒng)計的總字?jǐn)?shù)是一致的流炕。

基于我本地的環(huán)境先看看單線程運行的結(jié)果:

image

總計為:414142 字。

接下來換為多線程的方式:

List<String> allFile = scannerFile.getAllFile(strings[0]);
logger.info("allFile size=[{}]",allFile.size());
for (String msg : allFile) {
    executorService.execute(new ScanNumTask(msg,filterProcessManager));
}

public class ScanNumTask implements Runnable {

    private static Logger logger = LoggerFactory.getLogger(ScanNumTask.class);

    private String path;

    private FilterProcessManager filterProcessManager;

    public ScanNumTask(String path, FilterProcessManager filterProcessManager) {
        this.path = path;
        this.filterProcessManager = filterProcessManager;
    }

    @Override
    public void run() {
        Stream<String> stringStream = null;
        try {
            stringStream = Files.lines(Paths.get(path), StandardCharsets.UTF_8);
        } catch (Exception e) {
            logger.error("IOException", e);
        }

        List<String> collect = stringStream.collect(Collectors.toList());
        for (String msg : collect) {
            filterProcessManager.process(msg);
        }
    }
}

使用線程池管理線程仅胞,更多線程池相關(guān)的內(nèi)容請看這里:《如何優(yōu)雅的使用和理解線程池》

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


image

我們會發(fā)現(xiàn)無論執(zhí)行多少次浪感,這個值都會小于我們的預(yù)期值。

來看看統(tǒng)計那里是怎么實現(xiàn)的饼问。

@Component
public class TotalWords {
    private long sum = 0 ;

    public void sum(int count){
        sum += count;
    }

    public long total(){
        return sum;
    }
}

可以看到就是對一個基本類型進行累加而已影兽。那導(dǎo)致這個值比預(yù)期小的原因是什么呢?

我想大部分人都會說:多線程運行時會導(dǎo)致有些線程把其他線程運算的值覆蓋莱革。

但其實這只是導(dǎo)致這個問題的表象峻堰,根本原因還是沒有講清楚讹开。

內(nèi)存可見性

核心原因其實是由 Java 內(nèi)存模型(JMM)的規(guī)定導(dǎo)致的。

這里引用一段之前寫的《你應(yīng)該知道的 volatile 關(guān)鍵字》一段解釋:

由于 Java 內(nèi)存模型(JMM)規(guī)定捐名,所有的變量都存放在主內(nèi)存中旦万,而每個線程都有著自己的工作內(nèi)存(高速緩存)。

線程在工作時镶蹋,需要將主內(nèi)存中的數(shù)據(jù)拷貝到工作內(nèi)存中成艘。這樣對數(shù)據(jù)的任何操作都是基于工作內(nèi)存(效率提高),并且不能直接操作主內(nèi)存以及其他線程工作內(nèi)存中的數(shù)據(jù)贺归,之后再將更新之后的數(shù)據(jù)刷新到主內(nèi)存中淆两。

這里所提到的主內(nèi)存可以簡單認為是堆內(nèi)存,而工作內(nèi)存則可以認為是棧內(nèi)存拂酣。

如下圖所示:

image

所以在并發(fā)運行時可能會出現(xiàn)線程 B 所讀取到的數(shù)據(jù)是線程 A 更新之前的數(shù)據(jù)秋冰。

更多相關(guān)內(nèi)容就不再展開了,感興趣的朋友可以翻翻以前的博文婶熬。

直接來說如何解決這個問題吧剑勾,JDK 其實已經(jīng)幫我們想到了這些問題。

java.util.concurrent 并發(fā)包下有許多你可能會使用到的并發(fā)工具赵颅。

這里就非常適合 AtomicLong虽另,它可以原子性的對數(shù)據(jù)進行修改。

來看看修改后的實現(xiàn):

@Component
public class TotalWords {
    private AtomicLong sum = new AtomicLong() ;
    
    public void sum(int count){
        sum.addAndGet(count) ;
    }

    public  long total(){
        return sum.get() ;
    }
}

只是使用了它的兩個 API 而已饺谬。再來運行下程序會發(fā)現(xiàn)結(jié)果居然還是不對捂刺。

image

甚至為 0 了。

線程間通信

這時又出現(xiàn)了一個新的問題商蕴,來看看獲取總計數(shù)據(jù)是怎么實現(xiàn)的叠萍。

List<String> allFile = scannerFile.getAllFile(strings[0]);
logger.info("allFile size=[{}]",allFile.size());
for (String msg : allFile) {
    executorService.execute(new ScanNumTask(msg,filterProcessManager));
}

executorService.shutdown();
long total = totalWords.total();
long end = System.currentTimeMillis();
logger.info("total sum=[{}],[{}] ms",total,end-start);

不知道大家看出問題沒有,其實是在最后打印總數(shù)時并不知道其他線程是否已經(jīng)執(zhí)行完畢了绪商。

因為 executorService.execute() 會直接返回苛谷,所以當(dāng)打印獲取數(shù)據(jù)時還沒有一個線程執(zhí)行完畢,也就導(dǎo)致了這樣的結(jié)果格郁。

關(guān)于線程間通信之前我也寫過相關(guān)的內(nèi)容:《深入理解線程通信》

大概的方式有以下幾種:

image

這里我們使用線程池的方式:

在停用線程池后加上一個判斷條件即可:

executorService.shutdown();
while (!executorService.awaitTermination(100, TimeUnit.MILLISECONDS)) {
    logger.info("worker running");
}
long total = totalWords.total();
long end = System.currentTimeMillis();
logger.info("total sum=[{}],[{}] ms",total,end-start);

這樣我們再次嘗試腹殿,發(fā)現(xiàn)無論多少次結(jié)果都是正確的了:

image

效率提升

可能還會有朋友問,這樣的方式也沒見提升多少效率啊例书。

這其實是由于我本地文件少锣尉,加上一個文件處理的耗時也比較短導(dǎo)致的。

甚至線程數(shù)開的夠多導(dǎo)致頻繁的上下文切換還是讓執(zhí)行效率降低决采。

為了模擬效率的提升自沧,每處理一個文件我都讓當(dāng)前線程休眠 100 毫秒來模擬執(zhí)行耗時。

先看單線程運行需要耗時多久。

image

總共耗時:[8404] ms

接著在線程池大小為 4 的情況下耗時:

image
image

總共耗時:[2350] ms

可見效率提升還是非常明顯的拇厢。

更多思考

這只是多線程其中的一個用法爱谁,相信看到這里的朋友應(yīng)該多它的理解更進一步了。

再給大家留個閱后練習(xí)孝偎,場景也是類似的:

在 Redis 或者其他存儲介質(zhì)中存放有上千萬的手機號碼數(shù)據(jù)访敌,每個號碼都是唯一的,需要在最快的時間內(nèi)把這些號碼全部都遍歷一遍衣盾。

有想法感興趣的朋友歡迎在文末留言參與討論????寺旺。

總結(jié)

希望看完的朋友心中能對文初的幾個問題能有自己的答案:

  • 為什么需要多線程?
  • 怎么實現(xiàn)一個多線程程序势决?
  • 多線程帶來的問題及解決方案阻塑?

文中的代碼都在此處。

https://github.com/crossoverJie/NOWS

你的點贊與轉(zhuǎn)發(fā)是最大的支持徽龟。

image
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末叮姑,一起剝皮案震驚了整個濱河市唉地,隨后出現(xiàn)的幾起案子据悔,更是在濱河造成了極大的恐慌,老刑警劉巖耘沼,帶你破解...
    沈念sama閱讀 210,914評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件极颓,死亡現(xiàn)場離奇詭異,居然都是意外死亡群嗤,警方通過查閱死者的電腦和手機菠隆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評論 2 383
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來狂秘,“玉大人骇径,你說我怎么就攤上這事≌叽海” “怎么了破衔?”我有些...
    開封第一講書人閱讀 156,531評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長钱烟。 經(jīng)常有香客問我晰筛,道長,這世上最難降的妖魔是什么拴袭? 我笑而不...
    開封第一講書人閱讀 56,309評論 1 282
  • 正文 為了忘掉前任读第,我火速辦了婚禮,結(jié)果婚禮上拥刻,老公的妹妹穿的比我還像新娘怜瞒。我一直安慰自己,他們只是感情好般哼,可當(dāng)我...
    茶點故事閱讀 65,381評論 5 384
  • 文/花漫 我一把揭開白布吴汪。 她就那樣靜靜地躺著尘吗,像睡著了一般。 火紅的嫁衣襯著肌膚如雪浇坐。 梳的紋絲不亂的頭發(fā)上睬捶,一...
    開封第一講書人閱讀 49,730評論 1 289
  • 那天,我揣著相機與錄音近刘,去河邊找鬼择镇。 笑死,一個胖子當(dāng)著我的面吹牛苹丸,可吹牛的內(nèi)容都是我干的座咆。 我是一名探鬼主播,決...
    沈念sama閱讀 38,882評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼案淋,長吁一口氣:“原來是場噩夢啊……” “哼座韵!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起踢京,我...
    開封第一講書人閱讀 37,643評論 0 266
  • 序言:老撾萬榮一對情侶失蹤誉碴,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后瓣距,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體黔帕,經(jīng)...
    沈念sama閱讀 44,095評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,448評論 2 325
  • 正文 我和宋清朗相戀三年蹈丸,在試婚紗的時候發(fā)現(xiàn)自己被綠了成黄。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,566評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡逻杖,死狀恐怖奋岁,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情荸百,我是刑警寧澤闻伶,帶...
    沈念sama閱讀 34,253評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站管搪,受9級特大地震影響虾攻,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜更鲁,卻給世界環(huán)境...
    茶點故事閱讀 39,829評論 3 312
  • 文/蒙蒙 一霎箍、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧澡为,春花似錦漂坏、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽谷徙。三九已至,卻和暖如春驯绎,著一層夾襖步出監(jiān)牢的瞬間完慧,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評論 1 264
  • 我被黑心中介騙來泰國打工剩失, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留屈尼,地道東北人。 一個月前我還...
    沈念sama閱讀 46,248評論 2 360
  • 正文 我出身青樓拴孤,卻偏偏與公主長得像脾歧,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子演熟,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,440評論 2 348

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