软件事务内存

对象模型的缺陷

作为一个java程序员,我们对面向对象的编程(OOP)自然都是烂熟于胸的,但语言也极大地影响了我们对面向对象应用程序的建模方式。现在的OOP已经和AlanKay当初创造这个词时候的初衷大不相同了。他的主要思想是采用消息传递并消灭所有数据(他认为,系统是由一些类似于生物细胞那样的对象构成的,这些对象通过消息传递进行通信,且无需持有任何状态)。随着这一技术的演进,面向对象的语言开始朝着通过抽象数据类型(ADT)来实现数据隐藏(data hiding)的方向发展,并将数据和处理过程绑定或将状态与行为组合在一起。这在很大程度上引领我们走向封装和不断变化的状态。在这个过程中,我们最终还是把状态与实体(identity)进行了融合,即把对象实例与其数据整合在一起。

对于Java程序员来说,实体与状态的融合是在潜移默化间悄悄完成的,其结果可能不是很明显。当我们顺着指针或引用找到某个实例的时候,实际上是登录到了持有其状态的一块内存上,于是在那个位置上操纵数据也就成了自然而然的事了。该位置即代表了对象实例及其所包含的数据。将实体与状态进行合并最初看起来是非常简单且易于理解的,但从并发的角度来看,这种做法其实有很多严重的不良后果。

我们曾经被告知说面向对象的编程是对真实世界的建模。但悲催的是,真实世界与OO范式所试图构建的模型实际是大相径庭的。因为在真实的世界中,状态是不变的,而实体却是不断变化的。

将实体与状态分离

软件事务内存 STM

Clojure的STM采用了与数据库相似的多版本并发控制技术(MVCC),其并发控制也和数据库中的乐观锁(optimisticlocking)很像。当我们启动一个事务的时候,STM会记录一下时间戳,并将事务中将会用到所有ref都拷贝一份。由于状态是不可变的,所以对于ref的拷贝是多快好省的。当对某个不可变状态进行“变更”的时候,我们其实并没有改变它的值,而是为其创建了一个含有新值的拷贝。该拷贝是本事务的一个内部状态,并且由于我们使用了持久化的数据结构,这一步也是多快好省的。而如果STM识别出我们操作过的ref已经被别的事务改了的话,它就会中止并重做本事务。当事务成功完成时,所有的变更都会被写人内存,而时间戳也将被更新。

由于事务可能被重复执行多次,所以在写产品代码的时候请务必确保事务是幂等的并且没有任何副作用。这意味着在事务中控制台不能有任何输出、不能记录日志、也不能做任何不可逆操作。

总结

优点:

  • STM可以根据应用程序的行为来充分挖掘出其最大的并发潜力。也就是说,用了STM之后,我们可以无需使用过度保守的、需要预先定义的同步操作,而是让STM动态地管理竞争冲突。

  • STM是一种锁无关的编程模型,该模型可以提供良好的线程安全性和很高的并发性能。

  • STM可以保证实体仅能在事务内被更改。

  • STM没有显式锁意味着我们从此无需担心加锁顺序及其他相关问题。

  • STM没有显式锁的结果是无死锁的并发执行。

  • STM可以帮助我们减轻前期设计的决策负担,有了它我们就无需关心谁对什么东西上了锁,而只需放心地把这些工作交给动态隐式组合锁。

  • 该模型适用于对相同数据存在并发读且写冲突不频繁的应用场景。

缺点:

STM消除了显式的同步操作,所以我们在写代码时就无需担心自己是否忘了进行同步或是否在错误的级别上进行了同步。然而STM本身也存在一些问题,比如在跨越内存栅栏失败或遭遇竞争条件时我们捕获不到任何有用的信息。

STM只适用于写冲突非常少的应用场景,如果你的应用程序存在很多写操作竞争,那么我们就需要在STM之外寻找解决方案了。

转载请注明:转载自srzyhead的博客(https://srzyhead.github.io)

本文链接地址: java虚拟机并发编程 (6-软件事务内存)