各種程序員都工作在各自的程序抽象維度,如果我們發(fā)現(xiàn)解決一件事情比較難,也許是我們面對的抽象級別還不夠高,或者引入的間接程度不夠,本文以抽象角度來剖析并發(fā)編程。

  一、機(jī)器和OS級別抽象

  (1)馮諾伊曼模型

  經(jīng)典的順序化計(jì)算模型,貌似可以保證順序化一致性,但是沒有哪個(gè)現(xiàn)代的多處理架構(gòu)會(huì)提供順序一致性,馮氏模型只是現(xiàn)代多處理器行為的模糊近似。

  這個(gè)計(jì)算模型,指令或者命令列表改變內(nèi)存變量直接契合命令編程泛型,它以顯式的算法為中心,這和聲明式編程泛型有區(qū)別。就并發(fā)編程來說,會(huì)顯著的引入時(shí)間概念和狀態(tài)依賴

  所以所謂的函數(shù)式編程可以解決其中的部分問題。

  (2)進(jìn)程和線程

  進(jìn)程抽象運(yùn)行的程序,是操作系統(tǒng)資源分配的基本單位,是資源cpu,內(nèi)存,IO的綜合抽象。

  線程是進(jìn)程控制流的多重分支,它存在于進(jìn)程里,是操作系統(tǒng)調(diào)度的基本單位,線程之間同步或者異步執(zhí)行,共享進(jìn)程的內(nèi)存地址空間。

  (3)并發(fā)與并行

  并發(fā),英文單詞是concurrent,是指邏輯上同時(shí)發(fā)生,有人做過比喻,要完成吃完三個(gè)饅頭的任務(wù),一個(gè)人可以這個(gè)饅頭咬一口,那個(gè)饅頭咬一口,這樣交替進(jìn)行,最后吃完三個(gè)饅頭,這就是并發(fā),因?yàn)樵谌齻€(gè)饅頭上同時(shí)發(fā)生了吃的行為,如果只是吃完一個(gè)接著吃另一個(gè),這就不是并發(fā)了,是排隊(duì),三個(gè)饅頭如果分給三個(gè)人吃,這樣的任務(wù)完成形式叫并行,英文單詞是parallel.

  回到計(jì)算機(jī)概念,并發(fā)應(yīng)該是單CPU時(shí)代或者單核時(shí)代的說法,這個(gè)時(shí)候CPU要同時(shí)完成多任務(wù),只能用時(shí)間片輪轉(zhuǎn),在邏輯上同時(shí)發(fā)生,但在物理上是串行的。現(xiàn)在大多數(shù)計(jì)算機(jī)都是多核或者多CPU,那么現(xiàn)在的多任務(wù)執(zhí)行方式就是物理上并行的。

  為了從物理上支持并發(fā)編程,CPU提供了相應(yīng)的特殊指令,比如原子化的讀改寫,比較并交換。

  (4)平臺(tái)內(nèi)存模型

  在可共享內(nèi)存的多處理器體系結(jié)構(gòu)中,每個(gè)處理器都有它自己的緩存,并且周期性的與主存同步,為什么呢?因?yàn)樘幚砥魍ㄟ^降低一致性來換取性能,這和CAP原理通過降低一致性來獲取伸縮性有點(diǎn)類似,所以大量的數(shù)據(jù)在CPU的寄存器中被計(jì)算,另外CPU和編譯器為了性能還會(huì)亂序執(zhí)行,但是CPU會(huì)提供存儲(chǔ)關(guān)卡指令來保證存儲(chǔ)的同步,各種平臺(tái)的內(nèi)存模型或者同步指令可能不同,所以這里必須介入對內(nèi)存模型的抽象,JMM就是其中之一。

  二、編程模型抽象

  (1)基于線程模型

  (2)基于Actor模型

  (3)基于STM軟件事務(wù)內(nèi)存

  ……

  Java體系是一個(gè)基于線程模型的本質(zhì)編程平臺(tái),所以我們主要討論線程模型。

  三、并發(fā)單元抽象

  大多數(shù)并發(fā)應(yīng)用程序都是圍繞執(zhí)行任務(wù)進(jìn)行管理的,任務(wù)是抽象,離散的工作單元,所以編寫并發(fā)程序,首要工作就是提取和分解并行任務(wù)。一旦任務(wù)被抽象出來,他們就可以交給并發(fā)編程平臺(tái)去執(zhí)行,同時(shí)在任務(wù)抽象還有另一個(gè)重要抽象,那就是生命周期,一個(gè)任務(wù)的開始,結(jié)束,返回結(jié)果,都是生命周期中重要的階段。那么編程平臺(tái)必須提供有效安全的管理任務(wù)生命周期的API.

  四、線程模型

  線程模型是Java的本質(zhì)模型,它無所不在,所以Java開發(fā)必須搞清楚底層線程調(diào)度細(xì)節(jié),不搞清楚當(dāng)然就會(huì)有struts1,struts2的原理搞不清楚的基本災(zāi)難(比如在struts2的action中塞入狀態(tài),把struts2的action配成單例)。

  用線程來抽象并發(fā)編程,是比較低級別的抽象,所以難度就大一些,難度級別會(huì)根據(jù)我們的任務(wù)特點(diǎn)有以下幾個(gè)類別

  (1)任務(wù)非常獨(dú)立,不共享,這是最理想的情況,編程壓力為0.

  (2)共享數(shù)據(jù),壓力開始增大,必須引入鎖,Volatile變量,問題有活躍度和性能危險(xiǎn)。

  (3)狀態(tài)依賴,壓力再度增大,這時(shí)候我們基本上都是求助jdk 提供的同步工具。

  五、任務(wù)執(zhí)行

  任務(wù)是一個(gè)抽象體,如果被抽象了出來,下一步就是交給編程平臺(tái)去執(zhí)行,在Java中,描述任務(wù)的一個(gè)基本接口是Runnable,可是這個(gè)抽象太有限了,它不能返回值和拋受檢查異常,所以Jdk5.0有另外一個(gè)高級抽象Callable.

  任務(wù)的執(zhí)行在Jdk中也是一個(gè)底級別的Thread,線程有好處,但是大量線程就有大大的壞處,所以如果任務(wù)量很多我們并不能就創(chuàng)建大量的線程去服務(wù)這些任務(wù),那么Jdk5.0在任務(wù)執(zhí)行上做了抽象,將任務(wù)和任務(wù)執(zhí)行隔離在接口背后,這樣我們就可以引入比如線程池的技術(shù)來優(yōu)化執(zhí)行,優(yōu)化線程的創(chuàng)建。

  任務(wù)是有生命周期的,所以Jdk5.0提供了Future這個(gè)對象來描述對象的生命周期,通過這個(gè)future可以取到任務(wù)的結(jié)果甚至取消任務(wù)。

  六、鎖

  當(dāng)然任務(wù)之間共享了數(shù)據(jù),那么要保證數(shù)據(jù)的安全,必須提供一個(gè)鎖機(jī)制來協(xié)調(diào)狀態(tài),鎖讓數(shù)據(jù)訪問原子,但是引入了串行化,降低了并發(fā)度,鎖是降低程序伸縮性的原罪,鎖是引入上下文切換的主要原罪,鎖是引入死鎖,活鎖,優(yōu)先級倒置的絕對原罪,但是又不能沒有鎖,在Java中,鎖是一個(gè)對象,鎖提供原子和內(nèi)存可見性,Volatile變量提供內(nèi)存可見性不提供原子,原子變量提供可見性和原子,通過原子變量可以構(gòu)建無鎖算法和無鎖數(shù)據(jù)結(jié)構(gòu),但是這需要高高手才可以辦到。