用Akka/Multiverse STM实现并发
上面我们已经学习了如何在Clojure里使用STM,我猜你现在一定很好奇如何在Java代码中使用STM。而对于这一需求,我们有如下选择:
- 直接在Java中使用Clojure STM。方法非常简单,我们只需将事务的代码封装在一个Callable接口的实现中就行了,详情请参见第7章。
- 喜欢用注解(annotation)的开发者可能会更倾向于使用Multiverse的STM API.
- 除了STM之外,如果我们计划使用角色(actor),那么还可以考虑选择Akka库。
Multiverse是由Peter Veentjer主持开发的一个基于Java的STM实现。通过这个库,我们可以在Java代码中使用注解来标识事务边界。我们既可以用@TransactionalMethod注解将单个的方法标记为事务性的,也可以用@TransactionalObject注解将一个类的所有方法都标记为事务性的。为了与其他JVM上的语言进行集成,Multiverse还提供了一组丰富的API来控制事物的开始和结束。
Akka是一个由Jonas Boner主持开发的一个基于Scala的解决方案,该方案可以用于包括Java在内的很多其他运行于JVM上的语言。Akka不但提供了STM和基于角色(actor)的并发方案,还提供了将二者混合使用的选项。此外,Akka使用Multiverse作为其STM的实现并提供了ACI(ACID的子集)特性。
Akka的性能非常棒,并且由于它既支持STM又支持基于角色(actor)的模型(详情请参见第8章),本章我们将会用它来实现演示Java STM的例子。
Akka/Multiverse中的事务
Akka的Java版采用了Multiverse的Clojure风格的STM。与Java那繁冗的代码风格相比,Clojure风格的Akka不会强迫我们在能够修改可变实体之前就创建事务。如果我们没有主动提供事务,则Akka/Multiverse就会自动把访问请求封装在一个事务中。所以当我们处于事务之中时,Akka的ref与Clojure的ref的表现是相同的;而当我们位于事务之外时,Akka ref的表现则更像是Clojure的atom。换句话说,想要使变更同步且有序就必须使其在事务中完成,否则变更将是同步但无序的。在任何情况下,Akka都会保证对于ref的更改是原子的、隔离的且一致的,并同时提供了不同等级的协调粒度。
在Akka中,我们既可以用写代码的方式在事务层对事务进行配置,也可以通过配置文件在应用程序/JVM层进行配置。例如,我们可以将一个事务定义为只读(readonly),于是Akka将不再允许任何位于该事务范围内的Akka引用被修改。这样做的好处是,如果我们将一些不可变的事务设置为只读,则程序性能将会得到一定的提升。除此之外,我们还可以控制在冲突情况下事务的最大重试次数。当然,还有很多其他参数可供我们配置,详情请参阅Akka的帮助文档。
Akka扩展了Multiverse中的嵌套事务(请参见6.9节),所以我们能够很方便地在事务中调用启动其他事务的函数。默认情况下,这些内部事务或嵌套事务都是与其外部事务融为一体的。
使用Akka引用和事务
Clojure中的ref是在语言层定义的,而 Akka是一个公共类库所以不能依赖任何现有语言的支持。所以Akka在其akka.stm包中提供了一个托管事务引用(managed transactional reference)Ref和一些为原始类型而设的特殊类,如IntRef、LongRef等。Ref(以及所有原始类型的特化引用)代表指向类型T的一个不可变值的托管可变实体(managed mutable identity)。像Integer、Long、Double、String这些类型以及其他不可变类型都符合作为值对象的(value object)条件。如果我们用了自己定义的类,则必须保证这个类是不可变的。也就是说,这个自定义的类只能包含final字段。
我们可以创建一个Ref的实例作为托管事务引用,其值可以在初始化时指定或干脆不指定(默认为null)。如果想获得引用的当前值,可以使用get()函数。如果要使引用指向另一个可变实体,则可以使用swap()函数。这些调用可以在我们提供的事务里执行,但如果我们没提供事务的话,它们也可以在其各自的事务中运行。
当多个线程都试图更改同一个托管引用时,Akka可以保证只有一个变更可以写入内存而其他变更将全部重做。Akka有专门的事务工具负责管理事务跨越内存栅栏的过程。也就是说,Akka(通过Multiverse)保证了在事务中一个托管ref变更的提交会先于后续所有其他事务对该ref的读操作,即该变更对所有其他事务可见。