2019-11-03


前言:

  這個(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。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 姓名:劉偉勛 —寧波市鎮(zhèn)海承迪文具有限公司 【日精進(jìn)打卡第515天】 【知~學(xué)習(xí)】 《...
    君臨天下_7055閱讀 423評(píng)論 0 0
  • 也談?wù)Z文學(xué)習(xí) 沙區(qū)教研室 卿紅燕 聽(tīng)過(guò)這樣一個(gè)故事:很久以前,...
    41626a49b1c2閱讀 186評(píng)論 0 1
  • 也談?wù)Z文學(xué)習(xí) 卿紅燕 聽(tīng)過(guò)這樣一個(gè)故事:很久以前,一位病危的老酋長(zhǎng)...
    41626a49b1c2閱讀 530評(píng)論 0 0
  • 生活中有很多的不經(jīng)意。不經(jīng)意間愛(ài)了,不經(jīng)意間犯錯(cuò)了,不經(jīng)意間收獲了,不經(jīng)意間被罰了...... 因著急...
    N落花成蔭閱讀 156評(píng)論 0 1
  • “姐姐,你若想他,便去見(jiàn)他吧。你便是要將熙國(guó)給他,我也不在意,反正這熙國(guó)的天下都是你打下來(lái)的?!?今晚的夜色很深,...
    狐則閱讀 741評(píng)論 0 0

友情鏈接更多精彩內(nèi)容