STM 软件事务内存——本质是为提高并发,通过事务来管理内存的读写访问以避免锁的使用
对Java程序员来说,我们对面向对象的编程(OOP)自然都是烂熟于胸的,但语言也极大地影响了我们构建面向对象应用程序的方式。(现在的OOP已经和Alan Kay当初创造这个词时候的初衷大不相同了,他的主要思想是采用消息传递并消灭所有状态数据(他认为,系统是由一些类似于生物细胞那样的对象构成的,这些对象通过消息传递进行通信,且无需持有任何状态)——go语言)
对于Java程序员来说,当我们顺着指针或引用找到某个实例的时候,实际上是登录到了持有其状态的一块内存上,于是在那个位置上操纵数据也就成了自然而然的事了。该位置即代表了对象实例及其所包含的数据。将实体与状态进行合并最初看起来是非常简单且易于理解的,但从并发的角度来看,这种做法其实有很多严重的不良后果。例如,如果我们需要实现一个打印银行账户详情(资金数量、当前余额、交易信息、最小余额等等)的程序,我们就会碰到很多并发相关的问题。你会发现手头待处理的引用其实是一个随时都可能发生变化的状态的代理。所以当我们查看账户信息的时候,就需要通过加锁来阻止其他线程对账户内容进行修改,而这也必将导致并发度的大幅下降。但问题并不是从加锁的那一刻才开始出现的,而是在我们把账户的实体与其状态合并的时候就已经存在了。
我们曾经被告知说面向对象的编程是对真实世界的建模。但悲催的是,真实世界与OO范式所试图构建的模型实际是大相径庭的。因为在真实的世界中,状态是不变的,而实体却是不断变化的。接下来我们将讨论这种说法为何是正确的。
将实体与状态分离
你能快速告诉我Google的股价现在是多少吗?我们当然可以说从证券市场开市的那一刻起股价就是在不断变化的。举一个简单的例子,2010年12月10日Google的收盘价是592美元,并且这个数字已经被载入史册、是不会再改变了。当然,Google今天的股价和那天已经完全不同了。而如果过几分钟之后再来查看Google的股价(假设证券市场是开市的),我们就会看到一个不一样的值,但之前的那个值其实并没有改变。从现在开始,我们得改变一下我们对对象的认识,而这也将同时改变我们使用对象的方式。后面我们会看到,把对象的实体与其不可变状态值进行分离的做法将如何帮助我们实现锁无关编程、提高并发度、同时最大程度地降低竞争。
将实体与状态分离绝对是一个天才的构想,这是STM模型过程中所采用的一个非常关键的步骤。假定我们的Google股票对象由两部分组成:第一部分用于表示该股的实体,其中包含一个指向第二部分的指针。第二部分则包含了该股最新股价,其中保存股价的变量即为不可变状态,如图 1所示。
图 1 将可变实体部分与不可变状态值进行分离 |
一旦接收到一个新的股价信息,我们就可以将其放入历史价格指数中。由于旧的股价是不可变的,所以我们可以将其共享出去供所有线程访问。Google股票对象就可以多快好省地对外提供数据读取服务。而一旦有新的数据准备就绪之后,我们可以快速更改实体中的指针,以使其指向保存新股价的字段。实体与状态分离的做法对于并发来说也是一大福音。因为采用了这种方法之后,我们就可以不用阻塞任何查询股价的请求了。由于状态是不会变的,所以我们可以欣然将其指针传递给发出查询请求的线程。
STM中的事务
事务的概念源自于数据库管理系统(DBMS)中数据库事务的概念。在数据库管理系统中,事务必须满足ACID性质,即原子性,一致性,隔离性和持久性。原子性指的是事务中的动作要么全部执行,要么一个都不执行;一致性指的是任何时刻,数据库必须处于一致性状态,即必须满足某些预先设定的条件;隔离性是指一个事务不能看见其他未提交事务所涉及到的内部对象的状态,而持久性则是指一个已提交的事务对数据库系统的改变必须是永久的。由于STM中的数据是全都放在内存而不是数据库或文件系统里的,所以STM只提供了事务的前三个属性,而缺少了对持久性的支持。通过将对内存的访问封装在事务(transactions)中,STM消除了多线程内存同步过程中我们易犯的那些错误!
在Clojure语言中,STM实现采用了与数据库相似的多版本并发控制技术(MVCC),其并发控制也和数据库中的乐观锁(optimistic locking)很像。当我们启动一个事务的时候,STM会记录一下时间戳,并将事务中将会用到所有ref都拷贝一份。由于状态是不可变的,所以对于ref的拷贝是多快好省的。当对某个不可变状态进行“变更”的时候,我们其实并没有改变它的值(value),而是为其创建了一个含有新值的拷贝。该拷贝是本事务的一个内部状态,并且由于我们使用了持久化的数据结构,这一步也是多快好省的。而如果STM识别出我们操作过的ref已经被别的事务改了的话,它就会中止并重做本事务。当事务成功完成时,所有的变更都会被写入内存,而时间戳也将被更新!
STM中的事务实现
软件事务内存的实现包括原子对象(Atomic object)、冲突判决器(Conflict manager)。其中原子对象的实现是最重要的,它是各事务之间通信同步的媒介。原子对象的实现又分为顺序性实现和事务实现:其中事务实现还要要求实现同步和恢复(recovery)功能,同步功能即意味着要求有检测事务冲突的能力,而恢复功能则意味着需要在事务失败的时候将对象回滚到事务执行之前的状态。目前提出的原子对象一般是基于读/写冲突(Read/Write conflict)的机制:原子对象提供两个接口,一个为读接口,一个为写接口,通过读接口可以得到一个可以读的对象,而通过写接口则可以得到一个可以写的对象。为了检测冲突(即多个事务并发时的同步情况),事务中可以设立两个集合,一个为读集(Read set),一个为写集(Write set),分别记录该事务所要处理的读写原子对象集。如果一个事务的读集或写集与另一个事务的写集有交叉,则表明两个事务冲突,需要冲突判决器进一步采取决策。
总结:STM 软件事务内存——本质是为提高并发,通过事务来管理内存的读写访问以避免锁的使用!对于clojure,akka来说,只需将对事务内存的操作封装为事务,简化了并发编程而让程序员无需考虑复杂的事务同步问题!