關(guān)于JAVA并發(fā)
一談到Java并發(fā)編程,我們一般就會聯(lián)想起進程、線程、并行、并發(fā)等等概念。那么這些概念都代表什么呢?進程與線程有什么關(guān)系?并發(fā)與并行又是什么關(guān)系呢?
進程與線程
進程是指程序的一次動態(tài)執(zhí)行過程,通常我們說計算機中正在執(zhí)行的程序就是進程,每個程序都會對應(yīng)著一個進程。一個進程包含了從代碼加載到執(zhí)行完成的一個完整過程,它是操作系統(tǒng)資源分配最小單元。
而線程則是比進程更小的執(zhí)行單位,是CPU調(diào)度和分派的基本單位。每個進程至少有一個線程,反過來一個線程只能屬于一個進程,線程可以對進程所有的資源進行調(diào)度和運算。線程既可以由操作系統(tǒng)內(nèi)核來控制調(diào)度,也可以由用戶程序進行控制調(diào)度。
根據(jù)下圖可以看到,多個CPU會去執(zhí)行這三個進程。其中每個進程都包含著至少一個線程,比如進程1包含了四個線程,進程2和進程3包含一個線程。此外,每個進程都有自己的資源,而進程內(nèi)的所有線程都共享進程包含的資源。

進程與線程
并發(fā)與并行
并發(fā)和并行都可以是相對于進程或是線程來說。并發(fā)是指一個或若干個CPU對多個進程或線程之間進行多路復(fù)用,用簡單的語言來說就是CPU輪著執(zhí)行多個任務(wù),每個任務(wù)都執(zhí)行一小段時間,從宏觀上看起來就像是全部任務(wù)都在同時執(zhí)行一樣。并行則是指多個進程或線程同一時刻被執(zhí)行,這是真正意義上的同時執(zhí)行,它必須要有多個CPU的支持。如下圖是并發(fā)和并行的執(zhí)行時間圖。對于并發(fā)來說,線程一線執(zhí)行一段時間,然后線程二再執(zhí)行一段時間,接著線程三再執(zhí)行一段時間。每個線程都輪流得到CPU的執(zhí)行時間,這種情況下只需要一個CPU即能夠?qū)崿F(xiàn)。對于并行來說,線程一、線程二和線程三是同時執(zhí)行的,這種情況下需要三個CPU才能實現(xiàn)。并發(fā)和并行都提升了CPU的資源利用率。

并發(fā)與并行
而對于Java并發(fā),就是在Java平臺上實現(xiàn)來實現(xiàn)并發(fā)機制,Java平臺上提供了線程以及線程并發(fā)
多線程能提升執(zhí)行效率
前面我們了解到多線程可以實現(xiàn)并發(fā)和并行執(zhí)行,所以多線程能提升總體的效率。如果不支持多線程的話,那么當(dāng)某個執(zhí)行任務(wù)進入等待阻塞狀態(tài)時,則可能因為阻塞而導(dǎo)致運行效率低下。如下圖一中,一個請求任務(wù)發(fā)起請求后則開始等待響應(yīng),此時該線程占著CPU又不干活,從整個運行線上可以看到真正運行(綠色方塊)的時間很少,這就是運行效率低下。但在如果支持多線程并發(fā)的情況下,則可以在等待阻塞時去干其它的活。此外,多CPU環(huán)境下,如果一個任務(wù)能夠分解成多個小任務(wù),那么就能夠用多個CPU同時執(zhí)行它,這樣就能以更加快的速度完成任務(wù),畢竟單個CPU運行能力有限。如下圖二中,一旦將任務(wù)分解成三個小任務(wù)后,在多CPU環(huán)境下則能夠并行執(zhí)行,大大減少了整體執(zhí)行時間。

單線程阻塞狀態(tài)

多線程并行
多線程能提升用戶體驗
多線程也能提升用戶體驗,如果一個線程的任務(wù)既包含耗時的任務(wù)又包含用戶交互的任務(wù),那么則可能會導(dǎo)致用戶體驗很糟糕。如下圖,假如大家看到這些窗口一直在打轉(zhuǎn)又無法對其進行操作,是不是很難受?一個線程發(fā)起請求后開始等待請求結(jié)果,用戶界面則一直卡著沒響應(yīng)。我們可以通過多線程將任務(wù)分為請求任務(wù)和界面操作兩部分,這樣就能在請求后保持對界面操作的響應(yīng),以便提供更好的用戶體驗。

界面卡住
多線程讓編碼更難
天下沒有免費的午餐,多線程也是需要付出代價的。從編寫代碼的角度來看,多線程使得編碼變得更加復(fù)雜,本質(zhì)上這是因為多線程機制與現(xiàn)代計算機結(jié)構(gòu)所帶來的。縱使在編程語言設(shè)計專家的努力下,現(xiàn)在有很多簡化多線程編程的語言和模型,但相比于單線程來說多線程的編寫仍然復(fù)雜很多。數(shù)據(jù)從主存儲到CPU中間有若干層緩存和寄存器,而且多個線程可能訪問共享內(nèi)存,這就涉及到數(shù)據(jù)同步問題,從而增加了多線程編程的復(fù)雜性。此外,線程與線程之間的通信也比較麻煩,這也增加了多線程編碼的復(fù)雜性。總之,盡管很多編程語言嘗試為我們提供更便捷的多線程編程,但在語言層面仍然無法完全屏蔽掉多線程與計算機結(jié)構(gòu)的復(fù)雜性,所以不管我們使用什么語言都需要為多線程的編碼考慮得更多。

上下文切換與資源開銷
多線程除了增加編碼難度外,它還在執(zhí)行過程中帶來實際的損耗,包括資源開銷和上下文切換開銷。資源開銷主要包括其本身占用的內(nèi)存資源、執(zhí)行時線程本地棧開銷以及對這些線程進行管理的開銷。而上下文切換開銷則是因為CPU由一個線程切換到另外一個線程是需要做現(xiàn)場保護和現(xiàn)場恢復(fù)工作,包括線程標(biāo)識、寄存器內(nèi)存、線程狀態(tài)、線程優(yōu)先級、線程資源清單等等。如下圖所示,假設(shè)線程一和線程二由某個CPU執(zhí)行。線程一執(zhí)行一段時間后將相關(guān)信息保存到現(xiàn)場數(shù)據(jù)結(jié)構(gòu)中,而線程數(shù)據(jù)結(jié)構(gòu)存放在主存儲中,然后從線程二對應(yīng)的現(xiàn)場數(shù)據(jù)結(jié)構(gòu)中恢復(fù)線程二相關(guān)信息,完成現(xiàn)場恢復(fù)后線程二開始執(zhí)行。接下去的過程反過來,由線程二切換到線程一。其中可以看到由虛線分割的兩部分被標(biāo)為切換開銷,從完整的時序來看,這兩部分并非執(zhí)行線程的任務(wù),而是消耗在了現(xiàn)場的保護和恢復(fù)上了,這便是上下文切換的開銷。

上下文切換
在實踐中我們要綜合考慮多線程的優(yōu)缺點,不能一味的去追求多線程,在使用多線程之前我們必須去衡量多線程帶來的好處與代價。
更多Java并發(fā)原理可關(guān)注作者下面的專欄:
作者簡介:筆名seaboat,擅長人工智能、計算機科學(xué)、數(shù)學(xué)原理、基礎(chǔ)算法。出版書籍:圖解數(shù)據(jù)結(jié)構(gòu)與算法、Tomcat內(nèi)核設(shè)計剖析、圖解Java并發(fā)原理、人工智能原理科普。