前言
前段時(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 文件)
當(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)題及解決方案?
再談多線程之前先來(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
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
List
接下來(lái)便是讀取字?jǐn)?shù),同時(shí)要過(guò)濾一些特殊文本(比如我想過(guò)濾掉所有的空格、換行、超鏈接等)。
簡(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
*/
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í)行即可。
這樣一個(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é)果:
總計(jì)為:414142 字。
接下來(lái)?yè)Q為多線程的方式:
List
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
try {
stringStream = Files.lines(Paths.get(path), StandardCharsets.UTF_8);
} catch (Exception e) {
logger.error("IOException", e);
}
List
for (String msg : collect) {
filterProcessManager.process(msg);
}
}
}
使用線程池管理線程,更多線程池相關(guān)的內(nèi)容請(qǐng)看這里:《如何優(yōu)雅的使用和理解線程池》
執(zhí)行結(jié)果:
我們會(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)存。
如下圖所示:
所以在并發(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ì)。
甚至為 0 了。
線程間通信
這時(shí)又出現(xiàn)了一個(gè)新的問(wèn)題,來(lái)看看獲取總計(jì)數(shù)據(jù)是怎么實(shí)現(xiàn)的。
List
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)容:《深入理解線程通信》
大概的方式有以下幾種:
這里我們使用線程池的方式:
在停用線程池后加上一個(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é)果都是正確的了:
效率提升
可能還會(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í)多久。
總共耗時(shí):[8404] ms
接著在線程池大小為 4 的情況下耗時(shí):
總共耗時(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)知!