
前言:
這個(gè)是我一個(gè)粉絲問(wèn)我的問(wèn)題,一個(gè)剛從python轉(zhuǎn)Java的粉絲朋友。特此拿出來(lái)分享一下。希望能對(duì)大家在這塊有迷惑的有所幫助。以下是他的問(wèn)題
面試時(shí)多線程是Java繞不去的坎,就有幾個(gè)問(wèn)題
1.為什么多線程在Java中這么重要?
2.據(jù)說(shuō)多線程會(huì)出現(xiàn)難以排查的BUG,那么使用協(xié)程的話能否避免這些BUG呢?
3.go的協(xié)程是可以跑滿整個(gè)核心的,但Java是不是除非從語(yǔ)言底層改造,否則做不到這一點(diǎn)?
4.Kotlin支持協(xié)程,是否用起來(lái)比多線程好呢?
5.所以,學(xué)好Java中的多線程是否還有必要呢?
答:
1.為什么多線程在Java中這么重要
多線程在哪里都很重要
2.據(jù)說(shuō)多線程會(huì)出現(xiàn)難以排查的BUG,那么使用協(xié)程的話能否避免這些BUG呢
不能,其實(shí)協(xié)程需要學(xué)習(xí)的東西都更多。如果你學(xué)過(guò)C#的話,你去好好分析一個(gè)await函數(shù)里面的任意兩行代碼是不是執(zhí)行在同一個(gè)線程里的,以及為什么,初學(xué)者都得頭痛一陣子。然而你這個(gè)不知道的話,出了bug你連改都不會(huì)改。
用了協(xié)程你也不可避免會(huì)遇到數(shù)據(jù)共享的問(wèn)題,要共享你要么atom要么interlocked要么上鎖,這完全是多線程的內(nèi)容。這個(gè)時(shí)候你甚至得去弄明白協(xié)程跟線程在實(shí)現(xiàn)的時(shí)候是如何對(duì)應(yīng)的,不然就抓瞎。
3.go的協(xié)程是可以跑滿整個(gè)核心的,但Java是不是除非從語(yǔ)言底層改造,否則做不到這一點(diǎn)
Java吧這個(gè)任務(wù)交給你了,你行程序就行,你不行程序就不行。
4.Kotlin支持協(xié)程,是否用起來(lái)比多線程好呢
參見(jiàn)2
? ? 所以,學(xué)好Java中的多線程是否還有必要呢
學(xué)什么語(yǔ)言都有必要學(xué)多線程,除了不讓你開(kāi)線程的。(不是黑)
結(jié)論與思考
先說(shuō)結(jié)論:協(xié)程是非常值得學(xué)習(xí)的概念,它是多任務(wù)編程的未來(lái)。但是Java全力推進(jìn)這個(gè)事情的動(dòng)力并不大。
先返回到問(wèn)題的本源。當(dāng)我們希望引入?yún)f(xié)程,我們想解決什么問(wèn)題。我想不外乎下面幾點(diǎn):
節(jié)省資源,輕量,具體就是:
節(jié)省內(nèi)存,每個(gè)線程需要分配一段棧內(nèi)存,以及內(nèi)核里的一些資源
節(jié)省分配線程的開(kāi)銷(xiāo)(創(chuàng)建和銷(xiāo)毀線程要各做一次syscall)
節(jié)省大量線程切換帶來(lái)的開(kāi)銷(xiāo)
與NIO配合實(shí)現(xiàn)非阻塞的編程,提高系統(tǒng)的吞吐
使用起來(lái)更加舒服順暢(async+await,跑起來(lái)是異步的,但寫(xiě)起來(lái)感覺(jué)上是同步的)
我們分開(kāi)來(lái)講下。
1. 先說(shuō)內(nèi)存。拿Java Web編程舉例子,一個(gè)tomcat上的woker線程池的最大線程數(shù)一般會(huì)配置為50~500之間(目前springboot的默認(rèn)值給的200)。也就是說(shuō)同一時(shí)刻可以接受的請(qǐng)求最多也就是這么多。如果超過(guò)了最大值,請(qǐng)求直接打失敗拒絕處理。假如每個(gè)線程給128KB,500個(gè)線程放一起的內(nèi)存占用量大概是60+MB。如果真的有瓶頸,也許CPU,IO,帶寬,DB的CPU等會(huì)有瓶頸,但這點(diǎn)內(nèi)存量的增幅對(duì)于動(dòng)輒數(shù)個(gè)GB的Java運(yùn)行時(shí)進(jìn)程來(lái)說(shuō)似乎并不是什么大問(wèn)題。
2. 換一個(gè)場(chǎng)景,比如IM服務(wù)器,需要同時(shí)處理大量空閑的鏈接(可能要幾十萬(wàn),上百萬(wàn))。這時(shí)候用connection per thread就很不劃算了。但是可以直接改用netty去處理這類(lèi)問(wèn)題。你可以理解為NIO + woker thread大致就是一套“協(xié)程”,只不過(guò)沒(méi)有實(shí)現(xiàn)在語(yǔ)法層面,寫(xiě)起來(lái)不優(yōu)雅而已。問(wèn)題是,你的場(chǎng)景真的處理了并發(fā)幾十萬(wàn),上百萬(wàn)的連接嗎?
3. 再說(shuō)創(chuàng)建/銷(xiāo)毀線程的開(kāi)銷(xiāo)。這個(gè)問(wèn)題在Java里通過(guò)線程池得到了很好的解決。你會(huì)發(fā)現(xiàn)即便你用vert.x或者kotlin的協(xié)程,歸根到底也是要靠線程池工作的。goroutine相當(dāng)于設(shè)置一個(gè)全局的“線程池”,GOMAXPROCS就是線程池的最大數(shù)量;而Java可以自由設(shè)置多個(gè)不同的線程池(比如處理請(qǐng)求一套,異步任務(wù)另外一套等)。kotlin利用這個(gè)機(jī)制來(lái)構(gòu)建多個(gè)不同的協(xié)程scope。這看起來(lái)似乎會(huì)更靈活一點(diǎn)。
4. 然后是線程的切換開(kāi)銷(xiāo)。線程的切換實(shí)際上只會(huì)發(fā)生在那些“活躍”的線程上。對(duì)于類(lèi)似于Web的場(chǎng)景,大量的線程實(shí)際上因?yàn)镮O(發(fā)請(qǐng)求/讀DB)而掛起,根本不會(huì)參與OS的線程切換?,F(xiàn)實(shí)當(dāng)中一個(gè)最大200線程的服務(wù)器可能同一時(shí)刻的“活躍線程”總數(shù)只有數(shù)十而已。其開(kāi)銷(xiāo)沒(méi)有想象的那么大。為了避免過(guò)大的線程切換開(kāi)銷(xiāo),真正要防范的是同時(shí)有大量“活躍線程”。這個(gè)事情我自己上學(xué)的時(shí)候干過(guò),當(dāng)時(shí)是寫(xiě)了一個(gè)網(wǎng)絡(luò)模擬器。每一個(gè)節(jié)點(diǎn),每一個(gè)鏈路都由一個(gè)線程實(shí)現(xiàn)。模擬跑起來(lái)后,同時(shí)的活躍線程上千。當(dāng)時(shí)整個(gè)機(jī)器瞬間卡死,直到kill掉這個(gè)程序。
5. 此外說(shuō)說(shuō)與NIO的配合。在Java這個(gè)生態(tài)里Java NIO/Netty/Vert.X/rxJava/Akka可以任意選擇。一般來(lái)講,Netty可以解決絕大部分因?yàn)镮O的等待造成資源浪費(fèi)的問(wèn)題。Vert.X/rxJava??梢宰尦绦?qū)懙母印皟?yōu)雅”一點(diǎn)(見(jiàn)仁見(jiàn)智)。Akka就是Java世界里對(duì)“原教旨OO“的實(shí)現(xiàn),很有特色。的確,用NIO + completedFuture/handler/lambda不如async+await寫(xiě)起來(lái)舒服,但起碼是可以干活的。
6. 如果真的要較真Java的NIO用于業(yè)務(wù)的問(wèn)題,其核心痛點(diǎn)應(yīng)該是JDBC。這是個(gè)誕生了幾十年的,必須使用Blocking IO的DB交互協(xié)議。其上承載了Java龐大的生態(tài)和業(yè)務(wù)邏輯。Java要改自己的編程方式,必須得重新設(shè)計(jì)和實(shí)現(xiàn)JDBC,就像https://github.com/vert-x3/vertx-mysql-postgresql-client 那樣做。問(wèn)題是,社區(qū)里這種“異步JDBC”還沒(méi)有支持oracle、sql server等傳統(tǒng)DB。對(duì)mysql和postgres的支持還需要繼續(xù)趟坑~
7. 如果認(rèn)真閱讀上面這些需要“協(xié)程”解決的問(wèn)題,就會(huì)發(fā)現(xiàn)基本上都可以以各種方式解決。覺(jué)得線程耗資源,可以控制線程總數(shù),可以減少線程stack的大小,可以用線程池配置max和min idle等等。想要go的channel,可以上disruptor。可以說(shuō),Java這個(gè)生態(tài)里盡管沒(méi)有“協(xié)程”這個(gè)第一級(jí)別的概念,但是要解決問(wèn)題的工具并不缺。
8. Java僅僅是沒(méi)有解決”協(xié)程“在Java中的定義,以及“寫(xiě)得優(yōu)雅“這個(gè)問(wèn)題。從工程角度,“寫(xiě)得優(yōu)雅”的優(yōu)勢(shì)并沒(méi)有很多追新的人想象的那么關(guān)鍵。C#也并非因?yàn)橛辛薬sync await就搶了Java的市場(chǎng)分毫。而反過(guò)來(lái),如果java社區(qū)全力推進(jìn)這個(gè)事情,Java歷史上的生態(tài)的積累卻因?yàn)閰f(xié)程的出現(xiàn)而進(jìn)行大換血。想像一下如果沒(méi)有thread,也沒(méi)有ThreadLocal,@Transactional不起作用了,又沒(méi)有等價(jià)的工具,是不是很郁悶?這么看來(lái)怎么著都不是個(gè)劃算的事情。我想Oracle對(duì)此并不會(huì)有太大興趣。OpenJDK的loom能不能成,如果真的release多少Java程序員愿意使用,師母已呆。據(jù)我所知在9012年的今天,還有大量的Java6程序員。
9. 其他新的語(yǔ)言歷史包袱少,比較容易重新思考“什么是現(xiàn)代的multi-task編程的方式“這個(gè)大主題。kotlin的協(xié)程、go的goroutine、javascript的async await、python的asyncio、swift的GCD都給了各自的答案。如果真的想入坑Java這個(gè)體系的“協(xié)程”,就從kotlin開(kāi)始吧,畢竟可以混合編程。
最后說(shuō)一句,多線程容易出bug主要因?yàn)椋?/h4>
“搶占“式的線程切換 —— 你無(wú)法確定兩個(gè)線程訪問(wèn)數(shù)據(jù)的順序,一切都很隨機(jī)
“同步“不可組裝 —— 同步的代碼組裝起來(lái)也不同步,必須加個(gè)更大的同步塊
協(xié)程能不能避免容易出bug的缺陷,主要看能不能避免上面兩個(gè)問(wèn)題。如果協(xié)程底層用的還是線程池,兩個(gè)協(xié)程還是通過(guò)共享內(nèi)存通訊,那么多線程該出什么bug,多協(xié)程照樣出。javascript里不出這種bug是因?yàn)槠溆脩艟€程就一個(gè),不會(huì)出現(xiàn)線程切換,也不用同步;go是建議用channel做goroutine的通訊。如果go routine不用channel,而是用共享變量,并且沒(méi)有用Sync包控制一下,還是會(huì)出bug。