软件事务内存导论(九) 集合与事务

集合与事务

在我们努力学习这些示例的过程中,很容易就会忘记我们所要处理的值都必须是不可变的。只有实体才是可变的,而状态值则是不可变的。虽然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

文章转自 并发编程网-ifeve.com

时间: 2024-08-31 08:18:49

软件事务内存导论(九) 集合与事务的相关文章

软件事务内存导论(六)配置Akka事务

配置Akka事务 默认情况下,Akka为其相关的运行参数都设定了默认值,我们可以通过代码或配置文件akka.conf来更改这些默认设置.如果想了解如何指定或修改该配置文件位置的详细信息,请参阅Akka的文档. 针对单个事务,我们可以利用TransactionFactory在程序代码中更改其设置.下面就让我们用这种方式先后在Java和Scala中更改一些设置来为你展示如何实现设置的变更. 在Java中对事务进行配置 01 public  class  CoffeePot  { 02     pri

软件事务内存导论(二)软件事务内存

1.1    软件事务内存 将实体与状态分离的做法有助于STM(软件事务内存)解决与同步相关的两大主要问题:跨越内存栅栏和避免竞争条件.让我们先来看一下在Clojure上下文中的STM是什么样子,然后再在Java里面使用它. 通过将对内存的访问封装在事务(transactions)中,Clojure消除了内存同步过程中我们易犯的那些错误(见 <Programming Clojure>[Hal09]和<The Joy of Clojure>[FH11]).Clojure会敏锐地观察和

软件事务内存导论

前言 软件事务内存 用Akka/Multiverse STM实现并发 创建事务 创建嵌套事务 配置Akka事务 阻塞事务 提交和回滚事件 集合与事务 处理写偏斜异常 STM的局限性 文章转自 并发编程网-ifeve.com

软件事务内存导论(三)用Akka/Multiverse STM实现并发

用Akka/Multiverse STM实现并发 上面我们已经学习了如何在Clojure里使用STM,我猜你现在一定很好奇如何在Java代码中使用STM.而对于这一需求,我们有如下选择: 直接在Java中使用Clojure STM.方法非常简单,我们只需将事务的代码封装在一个Callable接口的实现中就行了,详情请参见第7章. 喜欢用注解(annotation)的开发者可能会更倾向于使用Multiverse的STM API. 除了STM之外,如果我们计划使用角色(actor),那么还可以考虑选

软件事务内存导论(五)创建嵌套事务

1.1    创建嵌套事务 在之前的示例中,每个用到事务的方法都是各自在其内部单独创建事务,并且事务所涉及的变动也都是各自独立提交的.但如果我们想要将多个方法里的事务调整成一个统一的原子操作的时候,上述做法就无能为力了,所以我们需要使用嵌套事务来实现这一目标. 通过使用嵌套事务,所有被主控函数调用的那些函数所创建的事务都会默认被整合到主控函数的事务中.除此之外,Akka/Multiverse还提供 了很多其他配置选项,如新隔离事务(new isolated transactions)等.总之,使

软件事务内存导论(十一)-STM的局限性

1.1    STM的局限性 STM消除了显式的同步操作,所以我们在写代码时就无需担心自己是否忘了进行同步或是否在错误的层级上进行了同步.然而STM本身也存在一些问题,比如在跨越内存栅栏失败或遭遇竞争条件时我们捕获不到任何有用的信息.我似乎可以听到你内心深处那个精明的程序员在抱怨"怎么会这样啊?".确实,STM是有其局限性的,否则本书写到这里就应该结束了.STM只适用于写冲突非常少的应用场景,如果你的应用程序存在很多写操作竞争,那么我们就需要在STM之外寻找解决方案了. 下面让我们进一

软件事务内存导论(四)创建事务

创建事务 我们创建事务的目的是为了协调针对多个托管引用的变更.事务将会保证这些变更是原子的,也就是说,所有的托管引用要么全部被提交要么全部被丢弃,所以在事务之外我们将不会看到有任何局部变更(partial changes)出现.此外,我们也可以用创建事务的方式来解决对单个ref先读后写所引发的相关问题. Akka是用Scala开发出来的,所以如果我们工作中用的是Scala的话,就可以直接幸福地享用Akka简洁明了的API了.对于那些日常工作中不能使用Scala开发的程序员,Akka同样也提供了一

软件事务内存导论(八)提交和回滚事件

提交和回滚事件 Java的try-catch-finally语法结构不但使我们可以安全地处理异常,还能够在程序抛出异常时选择性地执行一些代码.同样地,我们也可以控制程序在事务成功提交之后去执行某段代码,而当事务回滚时则去执行另一段代码.StmUtils中的deferred()和compensatiing()这两个函数分别提供了上述功能.特别地,在实现事务的过程中,为保证事务能顺利完成,我们通常会加入一些带副作用的逻辑,而deferred()函数则是一个执行所有这部分逻辑的绝佳地点. Java中的

软件事务内存导论(七)阻塞事务

阻塞事务--有意识地等待 我们经常会遇到这样一种情况,即某事务T能否成功完成依赖于某个变量是否发生了变化,并且由于这种原因所引起的事务运行失败也可能只是暂时性的.作为对这种暂时性失败的响应,我们可能会返回一个错误码并告诉事务T等待一段时间之后再重试.然而在事务T等待期间,即使其他任务已经更改了事务T所依赖的数据,事务T也没法立即感知到并重试了.为了解决这一问题,Akka为我们提供了一个简单的工具--retry(),该函数可以先将事务进行回滚,并将事务置为阻塞状态直到该事物所依赖的引用对象发生变化