前言:
這個是我一個粉絲問我的問題河胎,一個剛從python轉(zhuǎn)Java的粉絲朋友镊屎。特此拿出來分享一下妥色。希望能對大家在這塊有迷惑的有所幫助吨些。以下是他的問題
面試時多線程是Java繞不去的坎垄惧,就有幾個問題
1.為什么多線程在Java中這么重要刁愿?
2.據(jù)說多線程會出現(xiàn)難以排查的BUG,那么使用協(xié)程的話能否避免這些BUG呢到逊?
3.go的協(xié)程是可以跑滿整個核心的铣口,但Java是不是除非從語言底層改造,否則做不到這一點觉壶?
4.Kotlin支持協(xié)程脑题,是否用起來比多線程好呢?
5.所以铜靶,學(xué)好Java中的多線程是否還有必要呢叔遂?
答:
1.為什么多線程在Java中這么重要
多線程在哪里都很重要
2.據(jù)說多線程會出現(xiàn)難以排查的BUG,那么使用協(xié)程的話能否避免這些BUG呢
不能旷坦,其實協(xié)程需要學(xué)習(xí)的東西都更多掏熬。如果你學(xué)過C#的話,你去好好分析一個await函數(shù)里面的任意兩行代碼是不是執(zhí)行在同一個線程里的秒梅,以及為什么旗芬,初學(xué)者都得頭痛一陣子。然而你這個不知道的話捆蜀,出了bug你連改都不會改疮丛。
用了協(xié)程你也不可避免會遇到數(shù)據(jù)共享的問題幔嫂,要共享你要么atom要么interlocked要么上鎖,這完全是多線程的內(nèi)容誊薄。這個時候你甚至得去弄明白協(xié)程跟線程在實現(xiàn)的時候是如何對應(yīng)的履恩,不然就抓瞎。
3.go的協(xié)程是可以跑滿整個核心的呢蔫,但Java是不是除非從語言底層改造切心,否則做不到這一點
Java吧這個任務(wù)交給你了,你行程序就行片吊,你不行程序就不行绽昏。
4.Kotlin支持協(xié)程,是否用起來比多線程好呢
參見2
? ? 所以俏脊,學(xué)好Java中的多線程是否還有必要呢
學(xué)什么語言都有必要學(xué)多線程全谤,除了不讓你開線程的。(不是黑)
結(jié)論與思考
先說結(jié)論:協(xié)程是非常值得學(xué)習(xí)的概念爷贫,它是多任務(wù)編程的未來认然。但是Java全力推進這個事情的動力并不大。
先返回到問題的本源漫萄。當(dāng)我們希望引入?yún)f(xié)程卷员,我們想解決什么問題。我想不外乎下面幾點:
節(jié)省資源卷胯,輕量子刮,具體就是:
節(jié)省內(nèi)存,每個線程需要分配一段棧內(nèi)存窑睁,以及內(nèi)核里的一些資源
節(jié)省分配線程的開銷(創(chuàng)建和銷毀線程要各做一次syscall)
節(jié)省大量線程切換帶來的開銷
與NIO配合實現(xiàn)非阻塞的編程挺峡,提高系統(tǒng)的吞吐
使用起來更加舒服順暢(async+await,跑起來是異步的担钮,但寫起來感覺上是同步的)
我們分開來講下橱赠。
1. 先說內(nèi)存。拿Java Web編程舉例子箫津,一個tomcat上的woker線程池的最大線程數(shù)一般會配置為50~500之間(目前springboot的默認值給的200)狭姨。也就是說同一時刻可以接受的請求最多也就是這么多。如果超過了最大值苏遥,請求直接打失敗拒絕處理饼拍。假如每個線程給128KB,500個線程放一起的內(nèi)存占用量大概是60+MB田炭。如果真的有瓶頸师抄,也許CPU,IO教硫,帶寬叨吮,DB的CPU等會有瓶頸辆布,但這點內(nèi)存量的增幅對于動輒數(shù)個GB的Java運行時進程來說似乎并不是什么大問題。
2. 換一個場景茶鉴,比如IM服務(wù)器锋玲,需要同時處理大量空閑的鏈接(可能要幾十萬,上百萬)涵叮。這時候用connection per thread就很不劃算了惭蹂。但是可以直接改用netty去處理這類問題。你可以理解為NIO + woker thread大致就是一套“協(xié)程”围肥,只不過沒有實現(xiàn)在語法層面剿干,寫起來不優(yōu)雅而已。問題是穆刻,你的場景真的處理了并發(fā)幾十萬,上百萬的連接嗎杠步?
3. 再說創(chuàng)建/銷毀線程的開銷氢伟。這個問題在Java里通過線程池得到了很好的解決。你會發(fā)現(xiàn)即便你用vert.x或者kotlin的協(xié)程幽歼,歸根到底也是要靠線程池工作的朵锣。goroutine相當(dāng)于設(shè)置一個全局的“線程池”,GOMAXPROCS就是線程池的最大數(shù)量甸私;而Java可以自由設(shè)置多個不同的線程池(比如處理請求一套诚些,異步任務(wù)另外一套等)。kotlin利用這個機制來構(gòu)建多個不同的協(xié)程scope皇型。這看起來似乎會更靈活一點诬烹。
4. 然后是線程的切換開銷。線程的切換實際上只會發(fā)生在那些“活躍”的線程上弃鸦。對于類似于Web的場景绞吁,大量的線程實際上因為IO(發(fā)請求/讀DB)而掛起,根本不會參與OS的線程切換』8瘢現(xiàn)實當(dāng)中一個最大200線程的服務(wù)器可能同一時刻的“活躍線程”總數(shù)只有數(shù)十而已家破。其開銷沒有想象的那么大。為了避免過大的線程切換開銷购岗,真正要防范的是同時有大量“活躍線程”汰聋。這個事情我自己上學(xué)的時候干過,當(dāng)時是寫了一個網(wǎng)絡(luò)模擬器喊积。每一個節(jié)點烹困,每一個鏈路都由一個線程實現(xiàn)。模擬跑起來后注服,同時的活躍線程上千韭邓。當(dāng)時整個機器瞬間卡死措近,直到kill掉這個程序。
5. 此外說說與NIO的配合女淑。在Java這個生態(tài)里Java NIO/Netty/Vert.X/rxJava/Akka可以任意選擇瞭郑。一般來講,Netty可以解決絕大部分因為IO的等待造成資源浪費的問題鸭你。Vert.X/rxJava屈张。可以讓程序?qū)懙母印皟?yōu)雅”一點(見仁見智)袱巨。Akka就是Java世界里對“原教旨OO“的實現(xiàn)阁谆,很有特色。的確愉老,用NIO + completedFuture/handler/lambda不如async+await寫起來舒服场绿,但起碼是可以干活的。
6. 如果真的要較真Java的NIO用于業(yè)務(wù)的問題嫉入,其核心痛點應(yīng)該是JDBC焰盗。這是個誕生了幾十年的,必須使用Blocking IO的DB交互協(xié)議咒林。其上承載了Java龐大的生態(tài)和業(yè)務(wù)邏輯熬拒。Java要改自己的編程方式,必須得重新設(shè)計和實現(xiàn)JDBC垫竞,就像https://github.com/vert-x3/vertx-mysql-postgresql-client 那樣做澎粟。問題是,社區(qū)里這種“異步JDBC”還沒有支持oracle欢瞪、sql server等傳統(tǒng)DB活烙。對mysql和postgres的支持還需要繼續(xù)趟坑~
7. 如果認真閱讀上面這些需要“協(xié)程”解決的問題,就會發(fā)現(xiàn)基本上都可以以各種方式解決引有。覺得線程耗資源瓣颅,可以控制線程總數(shù),可以減少線程stack的大小譬正,可以用線程池配置max和min idle等等宫补。想要go的channel,可以上disruptor曾我》叟拢可以說,Java這個生態(tài)里盡管沒有“協(xié)程”這個第一級別的概念抒巢,但是要解決問題的工具并不缺贫贝。
8. Java僅僅是沒有解決”協(xié)程“在Java中的定義,以及“寫得優(yōu)雅“這個問題。從工程角度稚晚,“寫得優(yōu)雅”的優(yōu)勢并沒有很多追新的人想象的那么關(guān)鍵崇堵。C#也并非因為有了async await就搶了Java的市場分毫。而反過來客燕,如果java社區(qū)全力推進這個事情鸳劳,Java歷史上的生態(tài)的積累卻因為協(xié)程的出現(xiàn)而進行大換血。想像一下如果沒有thread也搓,也沒有ThreadLocal赏廓,@Transactional不起作用了,又沒有等價的工具傍妒,是不是很郁悶幔摸?這么看來怎么著都不是個劃算的事情。我想Oracle對此并不會有太大興趣颤练。OpenJDK的loom能不能成既忆,如果真的release多少Java程序員愿意使用,師母已呆嗦玖。據(jù)我所知在9012年的今天尿贫,還有大量的Java6程序員。
9. 其他新的語言歷史包袱少踏揣,比較容易重新思考“什么是現(xiàn)代的multi-task編程的方式“這個大主題。kotlin的協(xié)程匾乓、go的goroutine捞稿、javascript的async await、python的asyncio拼缝、swift的GCD都給了各自的答案娱局。如果真的想入坑Java這個體系的“協(xié)程”,就從kotlin開始吧咧七,畢竟可以混合編程衰齐。
最后說一句,多線程容易出bug主要因為:
“搶占“式的線程切換 —— 你無法確定兩個線程訪問數(shù)據(jù)的順序继阻,一切都很隨機
“同步“不可組裝 —— 同步的代碼組裝起來也不同步耻涛,必須加個更大的同步塊
協(xié)程能不能避免容易出bug的缺陷,主要看能不能避免上面兩個問題瘟檩。如果協(xié)程底層用的還是線程池抹缕,兩個協(xié)程還是通過共享內(nèi)存通訊,那么多線程該出什么bug墨辛,多協(xié)程照樣出卓研。javascript里不出這種bug是因為其用戶線程就一個,不會出現(xiàn)線程切換,也不用同步奏赘;go是建議用channel做goroutine的通訊寥闪。如果go routine不用channel,而是用共享變量磨淌,并且沒有用Sync包控制一下疲憋,還是會出bug。