Java多線程--基礎(chǔ)概念
必須知道的幾個概念
同步和異步
同步方法一旦開始,調(diào)用者必須等到方法調(diào)用返回后姨裸,才能執(zhí)行后續(xù)行為坯辩;而異步方法調(diào)用馁龟,一旦開始,方法調(diào)用就立即返回漆魔,調(diào)用者不用等待就可以繼續(xù)執(zhí)行后續(xù)行為坷檩。
更具體地說,同步調(diào)用就是調(diào)用者調(diào)用當前方法后改抡,就一直等待著該方法執(zhí)行完畢并返回結(jié)果矢炼;而異步調(diào)用是調(diào)用者調(diào)用方法后,可以不用等待阿纤,直接開始其他行為的執(zhí)行句灌,等到某一個時間點原來的方法執(zhí)行結(jié)束后要返回結(jié)果,會通知調(diào)用者阵赠。
并發(fā)和并行
- 并發(fā)是多個任務(wù)交替執(zhí)行涯塔,可以被中斷肌稻,但是任何時候只有一個任務(wù)在執(zhí)行。比如單核CPU交替執(zhí)行不同的指令匕荸。
- 并行是真正的同時執(zhí)行爹谭,比如多核CPU,每個CPU在同一時刻執(zhí)行一個命令榛搔,那么在這一時刻就有多條指令被同時執(zhí)行了诺凡。
臨界區(qū)
可以被多個線程使用的公共資源或共享資源。但是每一次只能有一個線程使用它践惑,一旦臨界區(qū)資源被(某一個線程)占用腹泌,其他線程若想要使用這個資源,就必須等待尔觉。
阻塞和非阻塞
某一個線程占用了臨界區(qū)資源凉袱,其他想要使用這個資源的線程就必須等待,等待導致線程掛起侦铜,這就是阻塞专甩。非阻塞與之相反:沒有一個線程可以妨礙其他線程的執(zhí)行。
死鎖钉稍、饑餓和活鎖
死鎖涤躲。都占用著對方想要的資源不釋放,如A占用B想要的不釋放贡未,B也占用著A的不釋放种樱,形成了“環(huán)”。
饑餓俊卤。一個或者多個線程無法獲得所需的資源嫩挤,遲遲不能得到執(zhí)行。饑餓有如下兩種情況:
- 當前線程的優(yōu)先級過低瘾蛋,高優(yōu)先級的線程不斷搶占它所需的資源俐镐,導致低優(yōu)先級的線程無法得到執(zhí)行矫限。
- 某一個線程一直占用著資源不釋放哺哼,導致其他線程都無法得到執(zhí)行。
饑餓有可能在未來的一段時間內(nèi)解決叼风,比如低優(yōu)先級的線程終于拿到了資源取董,或者線程執(zhí)行完成不再一直占用資源了。
活鎖无宿。線程都主動禮讓茵汰,將資源釋放給其他線程使用。比如A將資源給B孽鸡,B也禮讓給A蹂午,就導致了資源在A栏豺、B之間跳動,從而沒有一個線程可以拿到資源豆胸。就好像“踢皮球”奥洼。
并發(fā)級別
- 阻塞。其他線程釋放資源之前晚胡,當前線程無法繼續(xù)執(zhí)行灵奖。在試圖執(zhí)行后續(xù)代碼之前,必須要先得到臨界區(qū)的鎖估盘,如果得不到瓷患,線程就會被掛起等待。
- 無饑餓遣妥。如果線程之間有優(yōu)先級擅编,那么線程調(diào)度時總是傾向于滿足高優(yōu)先級的線程,即對同一個資源的分配是不公平的箫踩。對于不公平的鎖沙咏,高優(yōu)先級的線程可以插隊到低優(yōu)先級線程之前。低優(yōu)先級的線程就容易產(chǎn)生饑餓班套。如果資源分配是公平的肢藐,那么不論優(yōu)先級,嚴格遵循先來后到吱韭,饑餓就不會產(chǎn)生吆豹。
- 無障礙。此時所有線程都可以進入臨界區(qū)理盆,就有可能在同一時刻多個線程修改同一共享變量痘煤,導致將數(shù)據(jù)修改得面目全非。對于無障礙的線程來說猿规,一旦檢測到這種情況就會被已做的修改進行回滾衷快,確保數(shù)據(jù)的安全。一種無障礙的實現(xiàn)可以通過設(shè)置一個“一致性標志”來實現(xiàn):線程在操作之前姨俩,先讀取并保存這個標記蘸拔,等操作完成之后,再次讀取环葵,檢查標記是否被修改過调窍,如果兩者一致說明對資源的訪問沒有沖突;否則存在沖突张遭,任何對資源有修改操作的線程邓萨,在修改數(shù)據(jù)之前,先更新這個標記,以表明數(shù)據(jù)不再安全缔恳。無障礙可能出現(xiàn)如下現(xiàn)象:沒有一個線程能走出臨界區(qū)宝剖。
- 無鎖。無鎖的并行都是無障礙的歉甚。和 無障礙不同的是诈闺,無鎖的并發(fā)保證必然會有一個線程能在有限步內(nèi)完成操作離開臨界區(qū)。
- 無等待铃芦。無鎖只要求有一個線程可以在有限步內(nèi)完成操作雅镊,而無等待要求所有的線程都必須在有限步內(nèi)完成,這樣就避免了饑餓問題刃滓。一種典型的無等待結(jié)構(gòu)是RCU(Read-Copy-Update)仁烹,基本思想是對數(shù)據(jù)的讀操作不加控制,因此所有的讀線程都是無等待的咧虎,不會引發(fā)沖突卓缰;在寫數(shù)據(jù)時,先取得原始數(shù)據(jù)的副本砰诵,接著只修改副本(沒有直接修改原始數(shù)據(jù)征唬,所以讀操作才可以不加以控制),修改完副本后茁彭,在合適的時機回寫數(shù)據(jù)总寒。
兩個關(guān)于并行的定律
Amdahl定律
$$T_n = T_1(F + 1/n(1-F))$$
即
$$T_n = \frac{1}{F+1/n(1-F)}$$
$T_n$定義為加速比
加速比 = 優(yōu)化前系統(tǒng)耗時 / 優(yōu)化后系統(tǒng)耗時
其中$n$是處理器的個數(shù),F(xiàn)是串行化比例理肺。即必須串行的步驟數(shù) / 總步驟數(shù)摄闸,$F = 1$表示所有步驟只能串行完成,加速比為1(沒有加速)妹萨;$F = 0$表示所有步驟可以由多核同時執(zhí)行年枕,加速比為處理器個數(shù)$n$。
可以看出Amdahl定律優(yōu)化效果取決于處理器的個數(shù)乎完,以及串行化比例熏兄。CPU越多,串行化比例越低树姨,優(yōu)化效果越好摩桶。一味的增加CPU的個數(shù),不降低串行化比例的化娃弓,也無法提高系統(tǒng)性能典格。
Guastafon定律
$$s(n) = n - F(n - 1)$$
$S(n)$和上面一樣表示加速比岛宦,$n$和$F$的意義也與上面相同台丛。
Amdahl定律強調(diào):當串行比例F固定時,加速比是有上限的,無論增加多少CPU都不能突破該上限(上限可以計算極限得到)
Gustafson定律強調(diào):當串行化比例F固定時挽霉,只要串行化比例足夠蟹牢恕(可并行化的代碼比重足夠大),那么加速比可以隨著CPU的數(shù)量線性增長侠坎。(F是常數(shù)時蚁趁,S(n)是n的一次函數(shù))
JMM(Java內(nèi)存模型)
原子性
原子性指一個操作不可中斷:一個線程一旦開始,直到執(zhí)行完成都不會被其他線程干擾实胸。
可見性
可見性指當一個線程修改了某個共享變量的值他嫡,其他線程是否能立即知道這個修改。串行中不存在可見性問題庐完,因為某個線程修改了某個值钢属,在后續(xù)步驟中,其他線程讀取到這個值時门躯,一定時修改后的值了淆党。在并行程序中,如編譯器優(yōu)化的原因讶凉,在CPU1上對變量a進行了優(yōu)化染乌,將其緩存在cache或寄存器中,此時如果CPU2修改了a的實際值懂讯,CPU1可能無法意識到a已經(jīng)被修改荷憋,依然會讀取緩存里的舊值。
除了編譯器優(yōu)化褐望,指令重排以及編輯器優(yōu)化都可能產(chǎn)生可見性問題台谊。
有序性
代碼不按照本來的順序執(zhí)行,即寫在前面的代碼跑到了后面去執(zhí)行譬挚。指令的執(zhí)行順序亂了锅铅,這就是有序性問題。
程序在執(zhí)行時减宣,可能會進行指令重排盐须,重排后的順序依然保持著串行語義一致,但不保證在并行下語義也一致漆腌。
之所以要進行指令重排贼邓,是出于優(yōu)化的考慮,為了減少中斷流水線闷尿,即中斷產(chǎn)生的等待時間塑径,提高CPU處理性能。
Happen-Before規(guī)則
有些指令是可以重排的填具,有些指令是不可重排的统舀。下面是一些基本原則:
- 程序順序原則:一個線程內(nèi)保證語義的串行性匆骗,比如第二條語句依賴第一條語句的結(jié)果,那么就不能先執(zhí)行第二條再執(zhí)行第一條誉简。
- volatile原則:volatile變量的寫先于讀碉就,著保證了volatile變量的可見性
- 鎖規(guī)則:先解鎖,后續(xù)步驟再加鎖闷串。加鎖不能重排到解鎖之前瓮钥,這樣加鎖行為無法獲得鎖(剛加上就解了)
- 傳遞性:A先于B,B先于C烹吵,那么A先于C
- 線程的
start()
先于它的每個動作 - 線程的所有操作先于線程的終結(jié)(
Tread.join()
) - 線程的中斷(
interrupt()
)先于被中斷線程的代碼 - 對象的構(gòu)造函數(shù)執(zhí)行碉熄、結(jié)束先于
finalize()
方法。
by @sunhaiyu
2018.4.10