軟件事務內存導論(一)前言
請回憶一下你最近完成那個需要對共享可變變量進行同步的項目。在那個項目中,你肯定無法身心愉悅地享受出色地完成工作所帶來的樂趣,而是會陷入無盡 的質疑之中並抓狂地挨個確認是否在所有需要的地方都作了適當的同步。在過去所經曆過的編程工作中,我已經遇到過好幾次這樣令人神經衰弱的情況了,而其中絕 大部分原因都是由於我們在用Java編程的時候沒有遵循正確原則和方法來處理共享可變狀態。如果我們在某個該同步的地方忘了進行同步,那些不可預知的、潛 在的災難性的結果就將在不遠處等待著我們。但是人無完人,遺忘是我們的天性。所以我們應該充分利用工具來彌補我們自身的不足,同時也可以讓工具幫助我們實 現我們充滿創意的大腦所追求的那些偉大的目標,而不是讓錯誤一次次地打擊我們的信心。為了能夠得到可控的行為和結果,我們需要再次把目光投向JDK。
在本章中,我們將會通過使用Clojure中十分流行的軟件事務內存(STM)模型來學習如何線程安全地處理共享可變性(shared mutability)。在需要的時候,我們可能會在示例項目中混入Clojure的代碼。但是我們並非強迫你也要使用Clojure,因為隨著 Multiverse和Akka這些優秀工具的出現,我們也可以在Java中直接使用STM了。在本章中,我們會先來看看STM在Clojure裏是什麼 樣子,然後再學習如何用Java和Scala對事務內存進行編程。這種編程模型非常適用於那些讀多寫少的程序——它簡單易用並能提供可預測的結果。
1.1 同步與並發水火不容
同步操作本身就存在一些很基本的缺陷。
如果我們沒能處理好或幹脆就忘了進行同步,則某個線程所做的更改可能無法被其他線程及時感知。此外,為了同時保證可見性並避免競爭條件,我們還需要通過一些很麻煩的手段來進行同步操作。
不幸的是,當我們執行同步操作的時候,同時也強製了其他競爭相同資源的線程隻能等待。由於同步的粒度對並發度是有很大影響的,所以將同步控製的工作完全交由程序員來完將成會大大降低程序整體效率並增加錯誤發生的幾率。
同步操作還可能引發很多活躍度方麵的問題。由於某個線程可能吃著碗裏的看著鍋裏的,所以很容易造成程序死鎖。此外,同步還很容易造成活鎖(livelock)問題,即線程可能會在申請某一把鎖的時候不斷遭遇失敗。
當然,我們可以嚐試使用細粒度的鎖來提高程序並發度。雖然一般來說這個主意還不錯,但是其中最大的風險是程序員可能沒在合適的層級進行同步動作,因 為這太依賴於程序員的素質和責任心了。更糟的是,同步出問題的時候我們還收不到任何提示。此外,因為需要互斥訪問的線程加了鎖之後還是會阻塞其他線程的訪 問請求,所以細粒度的鎖隻是把線程等待的位置換了個地方而已。
熟練掌握JDK並發工具包的Java程序員在大城市裏一般都混的不錯。而且由於這麼長時間以來,我們在處理可變狀態的編程方麵都沒能找到一個比同步更合適的替代產品,所以導致了我們在這方麵的預期一直在不斷下降。但是新的編程模型終於還是到來了!
1.2 對象模型的缺陷
作為一個Java程序員,我們對麵向對象的編程(OOP)自然都是爛熟於胸的,但語言也極大地影響了我們構建麵向對象應用程序的方式。現在的OOP 已經和Alan Kay當初創造這個詞時候的初衷大不相同了。他的主要思想是采用消息傳遞並消滅所有狀態數據(他認為,係統是由一些類似於生物細胞那樣的對象構成的,這些 對象通過消息傳遞進行通信,且無需持有任何狀態)——見附錄2中《麵向對象編程的意義》一書。隨著這一技術的演進,麵向對象的語言開始朝著通過抽象數據類 型(ADTs)來實現數據隱藏(data hiding)的方向發展,並將數據和處理過程綁定或將狀態與行為組合在一起。這在很大程度上引領我們走向封裝和不斷變化的狀態。在這個過程中,我們最終 還是把狀態與實體(identity)進行了融合,即把對象實例與其數據整合在一起。
對於Java程序員來說,實體與狀態的融合是在潛移默化間悄悄完成的。當我們順著指針或引用找到某個實例的時候,實際上是登錄到了持有其狀態的一塊 內存上,於是在那個位置上操縱數據也就成了自然而然的事了。該位置即代表了對象實例及其所包含的數據。將實體與狀態進行合並最初看起來是非常簡單且易於理 解的,但從並發的角度來看,這種做法其實有很多嚴重的不良後果。例如,如果我們需要實現一個打印銀行賬戶詳情(資金數量、當前餘額、交易信息、最小餘額等 等)的程序,我們就會碰到很多並發相關的問題。你會發現手頭待處理的引用其實是一個隨時都可能發生變化的狀態的代理。所以當我們查看賬戶信息的時候,就需 要通過加鎖來阻止其他線程對賬戶內容進行修改,而這也必將導致並發度的大幅下降。但問題並不是從加鎖的那一刻才開始出現的,而是在我們把賬戶的實體與其狀 態合並的時候就已經存在了。
我們曾經被告知說麵向對象的編程是對真實世界的建模。但悲催的是,真實世界與OO範式所試圖構建的模型實際是大相徑庭的。因為在真實的世界中,狀態是不變的,而實體卻是不斷變化的。接下來我們將討論這種說法為何是正確的。
1.3 將實體與狀態分離
你能快速告訴我Google的股價現在是多少嗎?我們當然可以說從證券市場開市的那一刻起股價就是在不斷變化的,但這隻不過是一種文字遊戲罷了。舉 一個簡單的例子,2010年12月10日Google的收盤價是592美元,並且這個數字已經被載入史冊、是不會再改變了。而我們所要查找的隻是 Google股價當時的一個快照。當然,Google今天的股價和那天已經完全不同了。而如果過幾分鍾之後再來查看Google的股價(假設證券市場是開 市的),我們就會看到一個不一樣的值,但之前的那個值其實並沒有改變。從現在開始,我們得改變一下我們對對象的認識,而這也將同時改變我們使用對象的方 式。後麵我們會看到,把對象的實體與其不可變狀態值進行分離的做法將如何幫助我們實現鎖無關(lock-free)編程、提高並發度、同時最大程度地降低 競爭。
將實體與狀態分離絕對是一個天才的構想,這是Rich Hickey在其實現Clojure的STM模型過程中所采用的一個非常關鍵的步驟,詳情請見附錄2中的“值與變化—Clojure處理實體和狀態的方 法”。假定我們的Google股票對象由兩部分組成:第一部分用於表示該股的實體,其中包含一個指向第二部分的指針。第二部分則包含了該股最新股價,其中 保存股價的變量即為不可變狀態,如圖 6‑1所示。
![]() |
圖 6‑1 將可變實體部分與不可變狀態值進行分離 |
一旦接收到一個新的股價信息,我們就可以在不更改任何已存在事務的情況下將其放入曆史價格指數中。由於舊的股價是不可變的,所以我們可以將其共享出 去供所有線程訪問。正如我們在3.6節中所討論的那樣,如果我們在這裏采用持久化數據結構的話,則Google股票對象就可以多快好省地對外提供數據讀取 服務。而一旦有新的數據準備就緒之後,我們可以快速更改實體中的指針,以使其指向保存新股價的字段。
實體與狀態分離的做法對於並發來說也是一大福音。因為采用了這種方法之後,我們就可以不用阻塞任何查詢股價的請求了。由於狀態是不會變的,所以我們 可以欣然將其指針傳遞給發出查詢請求的線程。所有在我們更新實體(內部的指針——譯者注)之後到達的查詢請求都可以看到更新後的股價。我們知道,非阻塞的 讀操作即意味著更高的並發度,所以我們隻需要確保每個線程都能獲得一致的視圖即可。而這其中最棒的是,我們其實什麼也不用做,STM已經幫我們都搞定了。 相信你已經迫不及待想要了解更多關於STM的知識了吧?下麵就讓我們一起來學習這方麵的內容。
(未完待續)
文章轉自 並發編程網-ifeve.com
最後更新:2017-05-22 16:37:39