真实的国产乱ⅩXXX66竹夫人,五月香六月婷婷激情综合,亚洲日本VA一区二区三区,亚洲精品一区二区三区麻豆

成都創(chuàng)新互聯(lián)網(wǎng)站制作重慶分公司

一份針對(duì)于新手的多線程

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

創(chuàng)新互聯(lián)建站專注于企業(yè)營(yíng)銷型網(wǎng)站、網(wǎng)站重做改版、榆中網(wǎng)站定制設(shè)計(jì)、自適應(yīng)品牌網(wǎng)站建設(shè)、html5、成都商城網(wǎng)站開發(fā)、集團(tuán)公司官網(wǎng)建設(shè)、外貿(mào)網(wǎng)站建設(shè)、高端網(wǎng)站制作、響應(yīng)式網(wǎng)頁(yè)設(shè)計(jì)等建站業(yè)務(wù),價(jià)格優(yōu)惠性價(jià)比高,為榆中等各大城市提供網(wǎng)站開發(fā)制作服務(wù)。

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

于是在上周五通宵加班的空余時(shí)間寫了一個(gè)工具:

https://github.com/crossoverJie/NOWS

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

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

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

一份針對(duì)于新手的多線程
當(dāng)然結(jié)果看個(gè)樂(lè)就行(40 幾萬(wàn)字),因?yàn)樵缙诘牟┛臀蚁矚g大篇的貼代碼,還有一些英文單詞也沒(méi)有過(guò)濾,所以導(dǎo)致結(jié)果相差較大。

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

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

大部分新手開發(fā)都會(huì)去看多線程、但幾乎都沒(méi)有相關(guān)的實(shí)踐。甚至有些都不知道多線程拿來(lái)在實(shí)際開發(fā)中有什么用。

為此我想基于這個(gè)簡(jiǎn)單的工具為這類朋友帶來(lái)一個(gè)可實(shí)踐、易理解的多線程案例。

至少可以讓你知道:

為什么需要多線程?
怎么實(shí)現(xiàn)一個(gè)多線程程序?
多線程帶來(lái)的問(wèn)題及解決方案?

單線程統(tǒng)計(jì)

再談多線程之前先來(lái)聊聊單線程如何實(shí)現(xiàn)。

本次的需求也很簡(jiǎn)單,只是需要掃描一個(gè)目錄讀取下面的所有文件即可。

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

讀取某個(gè)目錄下的所有文件。
將所有文件的路徑保持到內(nèi)存。
遍歷所有的文件挨個(gè)讀取文本記錄字?jǐn)?shù)即可。
先來(lái)看前兩個(gè)如何實(shí)現(xiàn),并且當(dāng)掃描到目錄時(shí)需要繼續(xù)讀取當(dāng)前目錄下的文件。

這樣的場(chǎng)景就非常適合遞歸:

public List 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 ;

}

}

讀取之后將文件的路徑保持到一個(gè)集合中。

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

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

Stream stringStream = Files.lines(Paths.get(path), StandardCharsets.UTF_8);

List collect = stringStream.collect(Collectors.toList());

接下來(lái)便是讀取字?jǐn)?shù),同時(shí)要過(guò)濾一些特殊文本(比如我想過(guò)濾掉所有的空格、換行、超鏈接等)。

擴(kuò)展能力

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

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

所有這就需要一個(gè)比較靈活的處理方式。

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

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

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

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

public interface FilterProcess {

/**

  • 處理文本

  • @param msg

  • @return

*/

String process(String msg) ;

}

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

public class WrapFilterProcess implements FilterProcess{

@Override

public String process(String msg) {

msg = msg.replaceAll("\s*", "");

return msg ;

}

}

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

public class HttpFilterProcess implements FilterProcess{

@Override

public String process(String msg) {

msg = msg.replaceAll("^((https|http|ftp|rtsp|mms)?:\/\/)[^\s]+","");

return msg ;

}

}

這樣在初始化時(shí)需要將這些處理 handle 都加入責(zé)任鏈中,同時(shí)提供一個(gè) API 供客戶端執(zhí)行即可。
一份針對(duì)于新手的多線程
這樣一個(gè)簡(jiǎn)單的統(tǒng)計(jì)字?jǐn)?shù)的工具就完成了。

多線程模式
在我本地一共就幾十篇博客的條件下執(zhí)行一次還是很快的,但如果我們的文件是幾萬(wàn)、幾十萬(wàn)甚至上百萬(wàn)呢。

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

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

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

讀取某個(gè)目錄下的所有文件。
將文件路徑交由不同的線程自行處理。
最終匯總結(jié)果。
多線程帶來(lái)的問(wèn)題

也不是使用多線程就萬(wàn)事大吉了,先來(lái)看看第一個(gè)問(wèn)題:共享資源。

簡(jiǎn)單來(lái)說(shuō)就是怎么保證多線程和單線程統(tǒng)計(jì)的總字?jǐn)?shù)是一致的。

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

一份針對(duì)于新手的多線程
總計(jì)為:414142 字。

接下來(lái)?yè)Q為多線程的方式:

List 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 stringStream = null;

try {

stringStream = Files.lines(Paths.get(path), StandardCharsets.UTF_8);

} catch (Exception e) {

logger.error("IOException", e);

}

List collect = stringStream.collect(Collectors.toList());

for (String msg : collect) {

filterProcessManager.process(msg);

}

}

}

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

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

一份針對(duì)于新手的多線程
我們會(huì)發(fā)現(xiàn)無(wú)論執(zhí)行多少次,這個(gè)值都會(huì)小于我們的預(yù)期值。

來(lái)看看統(tǒng)計(jì)那里是怎么實(shí)現(xiàn)的。

@Component

public class TotalWords {

private long sum = 0 ;

public void sum(int count){

sum += count;

}

public long total(){

return sum;

}

}

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

我想大部分人都會(huì)說(shuō):多線程運(yùn)行時(shí)會(huì)導(dǎo)致有些線程把其他線程運(yùn)算的值覆蓋。

但其實(shí)這只是導(dǎo)致這個(gè)問(wèn)題的表象,根本原因還是沒(méi)有講清楚。

內(nèi)存可見性

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

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

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

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

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

如下圖所示:

一份針對(duì)于新手的多線程
所以在并發(fā)運(yùn)行時(shí)可能會(huì)出現(xiàn)線程 B 所讀取到的數(shù)據(jù)是線程 A 更新之前的數(shù)據(jù)。

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

直接來(lái)說(shuō)如何解決這個(gè)問(wèn)題吧,JDK 其實(shí)已經(jīng)幫我們想到了這些問(wèn)題。

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

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

來(lái)看看修改后的實(shí)現(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() ;

}

}

只是使用了它的兩個(gè) API 而已。再來(lái)運(yùn)行下程序會(huì)發(fā)現(xiàn)結(jié)果居然還是不對(duì)。

一份針對(duì)于新手的多線程
甚至為 0 了。

線程間通信

這時(shí)又出現(xiàn)了一個(gè)新的問(wèn)題,來(lái)看看獲取總計(jì)數(shù)據(jù)是怎么實(shí)現(xiàn)的。

List 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);

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

因?yàn)?executorService.execute() 會(huì)直接返回,所以當(dāng)打印獲取數(shù)據(jù)時(shí)還沒(méi)有一個(gè)線程執(zhí)行完畢,也就導(dǎo)致了這樣的結(jié)果。

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

大概的方式有以下幾種:

一份針對(duì)于新手的多線程
這里我們使用線程池的方式:

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

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);

這樣我們?cè)俅螄L試,發(fā)現(xiàn)無(wú)論多少次結(jié)果都是正確的了:

一份針對(duì)于新手的多線程
效率提升

可能還會(huì)有朋友問(wèn),這樣的方式也沒(méi)見提升多少效率啊。

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

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

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

先看單線程運(yùn)行需要耗時(shí)多久。

一份針對(duì)于新手的多線程
總共耗時(shí):[8404] ms

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

一份針對(duì)于新手的多線程
一份針對(duì)于新手的多線程
總共耗時(shí):[2350] ms

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

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

再給大家留個(gè)閱后練習(xí),場(chǎng)景也是類似的:

在 redis 或者其他存儲(chǔ)介質(zhì)中存放有上千萬(wàn)的手機(jī)號(hào)碼數(shù)據(jù),每個(gè)號(hào)碼都是唯一的,需要在最快的時(shí)間內(nèi)把這些號(hào)碼全部都遍歷一遍。

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

總結(jié)
希望看完的朋友心中能對(duì)文初的幾個(gè)問(wèn)題能有自己的答案:

為什么需要多線程?
怎么實(shí)現(xiàn)一個(gè)多線程程序?
多線程帶來(lái)的問(wèn)題及解決方案?

在這里給大家提供一個(gè)學(xué)習(xí)交流的平臺(tái),Java技術(shù)交流┟ 810309655

具有1-5工作經(jīng)驗(yàn)的,面對(duì)目前流行的技術(shù)不知從何下手,需要突破技術(shù)瓶頸的可以加群。

在公司待久了,過(guò)得很安逸,但跳槽時(shí)面試碰壁。需要在短時(shí)間內(nèi)進(jìn)修、跳槽拿高薪的可以加群。

如果沒(méi)有工作經(jīng)驗(yàn),但基礎(chǔ)非常扎實(shí),對(duì)java工作機(jī)制,常用設(shè)計(jì)思想,常用java開發(fā)框架掌握熟練的可以加群。


加Java架構(gòu)師進(jìn)階交流群獲取Java工程化、高性能及分布式、高性能、深入淺出。高架構(gòu)。
性能調(diào)優(yōu)、Spring,MyBatis,Netty源碼分析和大數(shù)據(jù)等多個(gè)知識(shí)點(diǎn)高級(jí)進(jìn)階干貨的直播免費(fèi)學(xué)習(xí)權(quán)限
都是大牛帶飛 讓你少走很多的彎路的 群號(hào)是: 810309655對(duì)了 小白勿進(jìn) 最好是有開發(fā)經(jīng)驗(yàn)

注:加群要求

1、具有工作經(jīng)驗(yàn)的,面對(duì)目前流行的技術(shù)不知從何下手,需要突破技術(shù)瓶頸的可以加。

2、在公司待久了,過(guò)得很安逸,但跳槽時(shí)面試碰壁。需要在短時(shí)間內(nèi)進(jìn)修、跳槽拿高薪的可以加。

3、如果沒(méi)有工作經(jīng)驗(yàn),但基礎(chǔ)非常扎實(shí),對(duì)java工作機(jī)制,常用設(shè)計(jì)思想,常用java開發(fā)框架掌握熟練的,可以加。

4、覺(jué)得自己很牛B,一般需求都能搞定。但是所學(xué)的知識(shí)點(diǎn)沒(méi)有系統(tǒng)化,很難在技術(shù)領(lǐng)域繼續(xù)突破的可以加。

5.阿里Java高級(jí)大牛直播講解知識(shí)點(diǎn),分享知識(shí),多年工作經(jīng)驗(yàn)的梳理和總結(jié),帶著大家全面、科學(xué)地建立自己的技術(shù)體系和技術(shù)認(rèn)知!


網(wǎng)站標(biāo)題:一份針對(duì)于新手的多線程
分享URL:http://weahome.cn/article/pocped.html

其他資訊

在線咨詢

微信咨詢

電話咨詢

028-86922220(工作日)

18980820575(7×24)

提交需求

返回頂部