引言
最近工作當中寫了一個有關並發的程序,引起了LZ對並發的強烈興趣。這一下一發不可收拾,LZ用了一個多星期,看完了這本共280+頁的並發編程書。之所以能看這么快,其實這主要歸功於,自己之前對並發就有一定的理解。在這種前提下看書,其實只是一個印證自己之前想法的過程而已,因此看起來會比較快,而且在看的時候,會有多次這種感覺,“擦,原來還真是這樣的”。
盡管LZ已經說了看書看的快的原因,但不管怎么說,書看的太快,肯定難免有遺漏。因此博客此時就派上用場了,它絕對可以幫你查缺補漏。因為在寫的過程中,你會發現,之前你讀的時候自以為理解的透透的東西,卻無法給別人講清楚。這就說明,你需要補補漏了。
並發的來源
並發的由來,從現在來看,似乎是必然的。因為人是一種懶惰而又急躁的動物,這是人的本性。因為急躁,並發就出現了,因為懶惰,多核時代就來了。
為什么這么說呢?
如果所有的程序都是串行的(之所以說如果,是因為LZ接觸到電腦時,它已經是並發的了,因此只能想象一下),那么當你打開了一個特別慢的網頁,最后你等不及想關掉瀏覽器的時候,你會發現,你必須等加載這個網頁的事做完你才能關閉它。這是何其蛋疼的一件事。急躁的人們能允許這種事發生?因此並發就出現了。
再來想象一下,如果我們想讓一個程序運行的更快,從直覺上講,我們應該讓CPU運算的更快。比如以前一秒可以計算1000次,現在我們讓它在一秒內可以計算10000次。但是懶惰的人們發現,這種直覺上的方式似乎非常困難,想要硬生生的提高CPU的速度(縮短時鍾周期)是非常困難的。因此懶惰的人們就想到一種偷懶的辦法,一個CPU一秒可以計算1000次,兩個的話不就可以計算2000次了(實際並非如此,但我們可以這么理解多個CPU帶來的效率增加),這個相對簡單的辦法最終被人們采用了。因此多核時代就到來了。
並發的危險
說起並發引起的危險,在LZ的理解來看,主要來源於程序所給人帶來的直覺造成的誤導。這一點在LZ所寫的計算機系統原理中有講到(可以將這兩本書的內容聯系起來),比如下面這個程序,它給人的直覺是,a首先變成了1,然后b變成了2。
int a = 1; int b = 2;
直覺是這樣的,但往往是錯誤的,因為在程序真正執行的時候,可能是b先變成了2,a又變成了1,更奇葩的是,很可能a和b都始終是0。估計說起兩者的賦值順序顛倒,各位還可以理解,但是說到兩者可能都是0,有的猿友就懵了,有種瞬間被顛覆三觀的感覺,但是學習並發往往就是顛覆你三觀的過程。
這種直覺與現實之間的不同,就給並發的程序造成了危險。它可能引起你預料之外的錯誤,而且往往是防不勝防。因此並發是誘人的,但也同樣是危險的,一個誘人的東西永遠都伴隨着危險,就像高貴的玫瑰往往都是有刺的。
知道了上面這些,我們就可以來看看安全性和活躍性了。安全性是指,“程序不會出現糟糕的事情”,活躍性則是指,“好的事情一定會發生”。可以看出來,安全性更多的是在強調執行的程序是正確的,而活躍性更多的是在強調程序可以正確的往下進行(有點繞?那就對了)。
舉個例子,對於一個並發遞歸求解的程序來講,安全性則可以保證結果的正確性,而活躍性則可以保證這個程序總能得到一個正確的結果或者最終發現它沒有解而拋出無解的異常。
Java的並發
對於大部分從事Java開發不久的程序猿來講(包括LZ),並發一般都是很少接觸到的(這里主要以LZ的領域來說,即J2EE),因為現有的框架已經將很多並發的問題給解決了,並給我們這些無腦程序猿創造了一個串行程序的環境。
比如,在J2EE領域的servlet規范當中,servlet是單例的,並且非常有可能,甚至可以說一定會被多個線程並發的去訪問。因此servlet其實是有並發的安全性問題的,除非你不在servlet當中記錄任何狀態。但是當前比較火的MVC框架struts2已經幫我們解決了這個問題,盡管Action當中經常會有一些數據或者說狀態,但Action在struts2中是非單例的,這相當於每個Action實例都是線程私有的,因此不存在並發問題。
有一些情況下,我們可能會接觸到並發問題,比如,你需要做一個單例的對象,那么這個對象一般可以被全局訪問,因此就可能存在並發的問題。可以這么說,幾乎所有采用了單例模式的對象都會涉及到並發的問題,除非這個對象沒有任何狀態,但是這往往不會出現,因為沒有狀態的單例對象是沒有意義的,它們更好的處理方式應該是一個無實例(即將構造函數私有化)且充滿了靜態方法的類。
很多程序猿在初次意識到並發時,都會采取一個看似萬能卻並非一定有用的方法,那就是將一個類的所有方法加上synchronized關鍵字。LZ以前也是這樣的,而且還自認為十分高端,現在想想,LZ實在自慚形穢。當時LZ對synchronized的理解,就知道它可以讓很多線程一個一個來執行這個方法,至於其它的特性,就不太明白了。
其實在大部分時候,一些比較簡單的場景中,上面這種無腦做法還是能起到相應的作用的,也就是說,它可以保證安全性與活躍性。但是另外一個特性就無法保證了,那就是性能。無腦的方法同步,有時候會將性能降低數個數量級,可能會使很多線程都在等待,CPU卻一直處於1%利用率的情況。
通常情況下,對於安全性、活躍性以及性能來說,我們會將性能放在最后一位,引用之前看過的一句經典的話,是用來形容面向對象設計的,即“可復用的前提是可用”。同樣的,對於並發的程序來講,“性能高的前提是程序的執行是正確的”。
小結
今天就暫且寫這么多吧,對於並發,其實想說的還有很多,畢竟剛看完這本書。后面還會陸續給出自己的理解,但是會穿插着《計算機系統原理系列》的內容,這個系列也該繼續前進了,因為並發已經耽誤了它太久。