1. 程式人生 > >軟體事務記憶體導論(九) 集合與事務

軟體事務記憶體導論(九) 集合與事務

宣告:本文是《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


丁 一

英文名ticmy,本站的翻譯主編,主要負責譯文的翻譯和校對工作,目前就職於阿里巴巴,對併發程式設計、JVM執行原理、正則等頗有興趣;個人部落格:http://www.ticmy.com/;同時,還是戶外攝影愛好者。