軟體事務記憶體導論(九) 集合與事務
宣告:本文是《Java虛擬機器併發程式設計》的第六章,感謝華章出版社授權併發程式設計網站釋出此文,禁止以任何形式轉載此文。
集合與事務
在我們努力學習這些示例的過程中,很容易就會忘記我們所要處理的值都必須是不可變的。只有實體才是可變的,而狀態值則是不可變的。雖然STM已經為我們減輕了很多負擔,但如果想要在維護不可變性的同時還要兼顧效能的話,對我們來說也將是一個非常嚴峻的挑戰。
為了保證不可變性,我們採取的第一個步驟是將單純用來儲存資料的類(value classes)及其內部所有成員欄位都置為final(在Scala中是val)。然後,我們需要傳遞地保證我們自己定義的類裡面的欄位所使用的類也都是不可變的。可以說,將欄位和類的定義置為final這一步是整個過程的基礎,這同時也是避免併發問題的第一步。
雖說不可變性可以使程式碼變得又好又安全,但是由於效能問題,程式設計師們還是不大願意使用這一特性。其癥結在於,為了維護不可變性,我們可能在資料沒發生任何變動的情況下也要進行拷貝操作,而這種無謂的拷貝對效能傷害很大。為了解決這個問題,我們在3.6節中曾經討論過持久化資料結構以及如何使用這類資料結構來減輕程式在效能方面的負擔。而在持久化資料結構的實現方面,已經有很多現成的第三方庫可供使用,而Scala本身也提供了這類資料結構。由於Java也有實現好的持久化資料結構可用,所以我們就無需專門為使用這個特性而去換用自己不熟悉的語言。
除了不可變性之外,我們還希望能獲得一些事務執行所需要的資料結構——這些資料結構的值是不可變的,但其實體可以在託管事務中被改變。Akka提供了兩種託管資料結構——TransactionalVector和TransactionalMap。這兩種資料結構源自於高效的Scala資料結構,其工作原理和Java的list、map類似。下面就讓我們一起來學習如何在Java和Scala中使用TransactionalMap
在Java中使用事務集合類
在Java中使用TransactionalMap是非常簡單的。例如,下面我們一起來寫一個為運動員們記錄得分的程式,其中對於得分的更新操作是併發執行的。這裡我們將不採用同步或鎖的方式,而是把所有更新操作都放在事務中處理。示例程式碼如下所示:
public class Scores { final private TransactionalMap<String, Integer> scoreValues = new TransactionalMap<String, Integer>(); final private Ref<Long> updates = new Ref<Long>(0L); public void updateScore(final String name, final int score) { new Atomic() { public Object atomically() { scoreValues.put(name, score); updates.swap(updates.get() + 1); if (score == 13) throw new RuntimeException("Reject this score"); return null; } }.execute(); } public Iterable<String> getNames() { return asJavaIterable(scoreValues.keySet()); } public long getNumberOfUpdates() { return updates.get(); } public int getScore(final String name) { return scoreValues.get(name).get(); } }
在updateScore()函式中,我們把設定某個運動員的得分以及增加更新次數的操作都收斂到一個事務裡面,該事務中所用到的TransactionalMap型別的scoreValue欄位以及Ref型別updates欄位都是託管型別。其中TransactionalMap支援普通Map的所有函式,只不過這些函式都是事務性的——即一旦事務回滾,我們對其進行的任何變更都將被丟棄。為了能夠觀察到實際的效果,我們人為地設定了一個回滾條件,即當得分為13的時,我們會先完成變更操作,然後拋異常令事務回滾。
在Java中,如果集合類實現了Iterable介面的話,我們就可以使用像for(String name: collectionOfNames)這樣的for-each語句。但TransactionalMap是一個Scala集合類,並且沒有直接支援這個介面。別擔心——Scala提供了一個叫做javaConversions的門面(façade設計模式——譯者注),該門面提供了很多方便的函式來獲取我們想要的Java介面。例如,我們可以使用asJavaIterable()函式來獲取原本需要使用getNames()函式才能拿到的介面。
至此我們已經完成了Scores類的全部功能,接下來我們還需要寫一個測試用例來檢驗Scores類所實現的這些功能:
package com.agiledeveloper.pcj; public class UseScores { public static void main(final String[] args) { final Scores scores = new Scores(); scores.updateScore("Joe", 14); scores.updateScore("Sally", 15); scores.updateScore("Bernie", 12); System.out.println("Number of updates: " + scores.getNumberOfUpdates()); try { scores.updateScore("Bill", 13); } catch(Exception ex) { System.out.println("update failed for score 13"); } System.out.println("Number of updates: " + scores.getNumberOfUpdates()); for(String name : scores.getNames()) { System.out.println( String.format("Score for %s is %d", name, scores.getScore(name))); } } }
上例中,我們先是添加了三個正常的運動員成績,隨後又增加了一個可以導致事務回滾的成績。但由於事務的存在,所以最後一個成績更新操作最終是無效的。而在程式碼的最後,我們會遍歷並輸出事務性map裡面的所有資料。下面讓我們觀察一下這段程式碼的輸出結果:
Number of updates: 3 update failed for score 13 Number of updates: 3 Score for Joe is 14 Score for Bernie is 12 Score for Sally is 15
在Scala中使用事務集合類
在Scala中,我們可以用與Java類似的方式來使用事務集合類。只不過由於這次是在Scala中,所以這裡我們需要使用Scala的內部迭代器而不是javaConversions門面(facade)。下面讓我們把Scores類翻譯成Scala程式碼:
class Scores { private val scoreValues = new TransactionalMap[String, Int]() private val updates = Ref(0L) def updateScore(name : String, score : Int) = { atomic { scoreValues.put(name, score) updates.swap(updates.get() + 1) if (score == 13) throw new RuntimeException("Reject this score") } } def foreach(codeBlock : ((String, Int)) => Unit) = scoreValues.foreach(codeBlock) def getNumberOfUpdates() = updates.get() }
如上所示,updateScore()函式與Java版本基本是相同的。唯一有點區別的地方是,我們去掉了getNames()函式和getScore()函式,併為foreach()提供了內部迭代器來遍歷map中的資料。我們在下面所列出了Scala版UseScores類的實現,這段程式碼是其Java版程式碼的直譯:
package com.agiledeveloper.pcj object UseScores { def main(args : Array[String]) : Unit = { val scores = new Scores() scores.updateScore("Joe", 14) scores.updateScore("Sally", 15) scores.updateScore("Bernie", 12) println("Number of updates: " + scores.getNumberOfUpdates()) try { scores.updateScore("Bill", 13) } catch { case ex => println("update failed for score 13") } println("Number of updates: " + scores.getNumberOfUpdates()) scores.foreach { mapEntry => val (name, score) = mapEntry println("Score for " + name + " is " + score) } } }
不出所料,測試用例的輸出結果也與Java版程式碼如出一轍:
Number of updates: 3 update failed for score 13 Number of updates: 3 Score for Joe is 14 Score for Bernie is 12 Score for Sally is 15