對Java程序員來說,我們對面向對象的編程(OOP)自然都是爛熟於胸的,但語言也極大地影響了我們構建面向對象應用程序的方式。(現在的OOP已經和Alan Kay當初創造這個詞時候的初衷大不相同了,他的主要思想是采用消息傳遞並消滅所有狀態數據(他認為,系統是由一些類似於生物細胞那樣的對象構成的,這些對象通過消息傳遞進行通信,且無需持有任何狀態)——go語言)
對於Java程序員來說,當我們順着指針或引用找到某個實例的時候,實際上是登錄到了持有其狀態的一塊內存上,於是在那個位置上操縱數據也就成了自然而然的事了。該位置即代表了對象實例及其所包含的數據。將實體與狀態進行合並最初看起來是非常簡單且易於理解的,但從並發的角度來看,這種做法其實有很多嚴重的不良后果。例如,如果我們需要實現一個打印銀行賬戶詳情(資金數量、當前余額、交易信息、最小余額等等)的程序,我們就會碰到很多並發相關的問題。你會發現手頭待處理的引用其實是一個隨時都可能發生變化的狀態的代理。所以當我們查看賬戶信息的時候,就需要通過加鎖來阻止其他線程對賬戶內容進行修改,而這也必將導致並發度的大幅下降。但問題並不是從加鎖的那一刻才開始出現的,而是在我們把賬戶的實體與其狀態合並的時候就已經存在了。
我們曾經被告知說面向對象的編程是對真實世界的建模。但悲催的是,真實世界與OO范式所試圖構建的模型實際是大相徑庭的。因為在真實的世界中,狀態是不變的,而實體卻是不斷變化的。接下來我們將討論這種說法為何是正確的。
將實體與狀態分離
你能快速告訴我Google的股價現在是多少嗎?我們當然可以說從證券市場開市的那一刻起股價就是在不斷變化的。舉一個簡單的例子,2010年12月10日Google的收盤價是592美元,並且這個數字已經被載入史冊、是不會再改變了。當然,Google今天的股價和那天已經完全不同了。而如果過幾分鍾之后再來查看Google的股價(假設證券市場是開市的),我們就會看到一個不一樣的值,但之前的那個值其實並沒有改變。從現在開始,我們得改變一下我們對對象的認識,而這也將同時改變我們使用對象的方式。后面我們會看到,把對象的實體與其不可變狀態值進行分離的做法將如何幫助我們實現鎖無關編程、提高並發度、同時最大程度地降低競爭。
將實體與狀態分離絕對是一個天才的構想,這是STM模型過程中所采用的一個非常關鍵的步驟。假定我們的Google股票對象由兩部分組成:第一部分用於表示該股的實體,其中包含一個指向第二部分的指針。第二部分則包含了該股最新股價,其中保存股價的變量即為不可變狀態,如圖 1所示。
![]() |
圖 1 將可變實體部分與不可變狀態值進行分離 |
一旦接收到一個新的股價信息,我們就可以將其放入歷史價格指數中。由於舊的股價是不可變的,所以我們可以將其共享出去供所有線程訪問。Google股票對象就可以多快好省地對外提供數據讀取服務。而一旦有新的數據准備就緒之后,我們可以快速更改實體中的指針,以使其指向保存新股價的字段。實體與狀態分離的做法對於並發來說也是一大福音。因為采用了這種方法之后,我們就可以不用阻塞任何查詢股價的請求了。由於狀態是不會變的,所以我們可以欣然將其指針傳遞給發出查詢請求的線程。
STM中的事務
事務的概念源自於數據庫管理系統(DBMS)中數據庫事務的概念。在數據庫管理系統中,事務必須滿足ACID性質,即原子性,一致性,隔離性和持久性。原子性指的是事務中的動作要么全部執行,要么一個都不執行;一致性指的是任何時刻,數據庫必須處於一致性狀態,即必須滿足某些預先設定的條件;隔離性是指一個事務不能看見其他未提交事務所涉及到的內部對象的狀態,而持久性則是指一個已提交的事務對數據庫系統的改變必須是永久的。由於STM中的數據是全都放在內存而不是數據庫或文件系統里的,所以STM只提供了事務的前三個屬性,而缺少了對持久性的支持。通過將對內存的訪問封裝在事務(transactions)中,STM消除了多線程內存同步過程中我們易犯的那些錯誤!
在Clojure語言中,STM實現采用了與數據庫相似的多版本並發控制技術(MVCC),其並發控制也和數據庫中的樂觀鎖(optimistic locking)很像。當我們啟動一個事務的時候,STM會記錄一下時間戳,並將事務中將會用到所有ref都拷貝一份。由於狀態是不可變的,所以對於ref的拷貝是多快好省的。當對某個不可變狀態進行“變更”的時候,我們其實並沒有改變它的值(value),而是為其創建了一個含有新值的拷貝。該拷貝是本事務的一個內部狀態,並且由於我們使用了持久化的數據結構,這一步也是多快好省的。而如果STM識別出我們操作過的ref已經被別的事務改了的話,它就會中止並重做本事務。當事務成功完成時,所有的變更都會被寫入內存,而時間戳也將被更新!
STM中的事務實現
軟件事務內存的實現包括原子對象(Atomic object)、沖突判決器(Conflict manager)。其中原子對象的實現是最重要的,它是各事務之間通信同步的媒介。原子對象的實現又分為順序性實現和事務實現:其中事務實現還要要求實現同步和恢復(recovery)功能,同步功能即意味着要求有檢測事務沖突的能力,而恢復功能則意味着需要在事務失敗的時候將對象回滾到事務執行之前的狀態。目前提出的原子對象一般是基於讀/寫沖突(Read/Write conflict)的機制:原子對象提供兩個接口,一個為讀接口,一個為寫接口,通過讀接口可以得到一個可以讀的對象,而通過寫接口則可以得到一個可以寫的對象。為了檢測沖突(即多個事務並發時的同步情況),事務中可以設立兩個集合,一個為讀集(Read set),一個為寫集(Write set),分別記錄該事務所要處理的讀寫原子對象集。如果一個事務的讀集或寫集與另一個事務的寫集有交叉,則表明兩個事務沖突,需要沖突判決器進一步采取決策。
總結:STM 軟件事務內存——本質是為提高並發,通過事務來管理內存的讀寫訪問以避免鎖的使用!對於clojure,akka來說,只需將對事務內存的操作封裝為事務,簡化了並發編程而讓程序員無需考慮復雜的事務同步問題!