继续完成"Software Transactional Memory"笔记的下篇,这部分内容基本上就是Clojure STM源码阅读指南,从事务实现的各种概念作为切入点,逐步跟进抽丝剥茧.在本文梳理即将结束的时候我找到"Software Transactional Memory"一文对应的Slide,看这个脉络更清晰一些,早点找到这个Slide会省一些力气.

 

源码阅读指南


Clojure STM 实现基本上都是Java代码实现的,依赖Java处理并发的基础设施:


Clojure STM的主要实现是在clojure.lang包的LockingTransaction类和 Ref类,路径:


  下面我们就从概念切入代码,把一个个概念落到实处,一图胜千言,阅读过程中可以不断回顾下面这张UML图.

 

创建事务

    创建一个Clojure的事务很简单就把一系列表达式放在dosync宏的内部执行即可.这比显示用锁可简单太多了.虽然这种方式不用显示指明哪些Ref可能在事务中改变,但开发者还是要好好考虑哪些逻辑要放在dosync里面执行,哪些Ref要保持一致的快照.

(def account1 (ref 100))
(def account2 (ref 0))

(defn transfer [amount from to]
(dosync
(alter from - amount) ; alter from => (- @from amount)
(alter to + amount))) ; alter to => (+ @to amount)


    dosync 将多个表达式构成的事务体(transaction body)传递给sync宏,在sync调用LockingTransaction的静态方法runInTransaction. sync将事务体封装在一个匿名方法传递给runInTransaction.每个线程都有一个LockingTransaction类型的ThreadLocal变量,如果LockingTransaction对象尚未创建就先创建.如果当前线程已经在事务中,匿名方法直接会在事务中执行,否则就会启动一个新的事务再执行事务体.

  看代码:

; dosync & sync 

(defmacro dosync
"Runs the exprs (in an implicit do) in a transaction that encompasses
exprs and any nested calls. Starts a transaction if none is already
running on this thread. Any uncaught exception will abort the
transaction and flow out of dosync. The exprs may be run more than
once, but any effects on Refs will be atomic."
{:added "1.0"}
[& exprs]
`(sync nil ~@exprs))


(defmacro sync
"transaction-flags => TBD, pass nil for now
Runs the exprs (in an implicit do) in a transaction that encompasses
exprs and any nested calls. Starts a transaction if none is already
running on this thread. Any uncaught exception will abort the
transaction and flow out of sync. The exprs may be run more than
once, but any effects on Refs will be atomic."
{:added "1.0"}
[flags-ignored-for-now & body]
`(. clojure.lang.LockingTransaction
(runInTransaction (fn [] ~@body))))

 //https://github.com/clojure/clojure/blob/master/src/jvm/clojure/lang/LockingTransaction.java

final static ThreadLocal<LockingTransaction> transaction = new ThreadLocal<LockingTransaction>();

static public Object runInTransaction(Callable fn) throws Exception{
LockingTransaction t = transaction.get();
if(t == null)
transaction.set(t = new LockingTransaction());

if(t.info != null)
return fn.call();

return t.run(fn);  
}

 

 

事务状态



事务状态有下面五种:

* RUNNING
* COMMITTING
* RETRY
* KILLED
* COMMITTED

  RETRY和KILLED要特别说明一下:

RETRY状态是指:事务开始尝试一次RETRY但是还没有进入Try的一个中间状态.Try开始之后状态是RUNNING.

private Object blockAndBail(Info refinfo){
//stop prior to blocking
stop(RETRY);
try
{
refinfo.latch.await(LOCK_WAIT_MSECS, TimeUnit.MILLISECONDS);
}
catch(InterruptedException e)
{
//ignore
}
throw retryex;
}


KILLED 有两种情况会导致状态被设置成KILLED,

1. 事务调用abort方法(目前的Clojure版本还没有代码调用abort).
2. 第二种情况就是抢先(barge)提交的时候


导致状态变为KILLED的两种情况,代码片段:

void abort() throws AbortException{
 stop(KILLED);
 throw new AbortException();
}


private boolean barge(Info refinfo){
boolean barged = false;
//if this transaction is older
// try to abort the other
if(bargeTimeElapsed() &&∓ startPoint < refinfo.startPoint)
{
 barged = refinfo.status.compareAndSet(RUNNING, KILLED);
if(barged)
 refinfo.latch.countDown();
}
return barged;
}

 

 

Committed Values



每个Ref object 都在其Tval字段维护了一串已经提交的值(变更历史,committed value chain). committed value chain的长度由字段minHistory(默认值0)和maxHistory(默认值10)决定,这个可以在创建Ref的时候自定义新值,或者后续使用ref-min-history和ref-max-history修改.看后面Faults的部分你就能了解这个长度控制的重要性了.

; pre-allocate history and limit max-history
;设置最大历史长度,如果长度已经大于ref-max-history值 并不执行收缩.
(def myref (ref 1 :min-history 5 :max-history: 10))

; supply a function to validate the ref before commit.
(def myvalidref (ref 1 :validator pos?))

 

 

Locks



     每一个Ref对象都有一个ReentrantReadWriteLock,多个并发事务可以获取其读锁,只有一个事务可以获取其写锁.有ensure的时候,会在事务执行全过程持有锁,ensure会一直持有读锁直到事务提交或者Ref在事务进行了修改(Ref上调用了ref-set或者alter,由于读锁无法直接升级为写锁,所以会先释放然后再获取写锁).没有什么情况是需要在整个事务过程中都要持有写锁的,写锁只是在需要的时候获取用完快速释放掉;在事务提交的时候会再次获取写锁提交完成后释放.ReentrantReadWriteLock的细节可以在Ref类的lock字段使用看到.

final HashSet<Ref> ensures = new HashSet<Ref>();   //all hold readLock

void doEnsure(Ref ref){
     if(!info.running())
          throw retryex;
     if(ensures.contains(ref))
          return;
     ref.lock.readLock().lock();

     //someone completed a write after our snapshot
     if(ref.tvals != null && ref.tvals.point > readPoint) {
        ref.lock.readLock().unlock();
        throw retryex;
    }

     Info refinfo = ref.tinfo;

     //writer exists
     if(refinfo != null && refinfo.running())
          {
          ref.lock.readLock().unlock();

          if(refinfo != info) //not us, ensure is doomed
               {
               blockAndBail(refinfo);
               }
          }
     else
          ensures.add(ref);
}


     在Ref上调用ref-set或者alter会产生in-transaction值,这个值在提交前不会被其它事务看到.这些调用还会触发tinfo字段被赋值; Ref中的LockingTransaction.Info tinfo;这个字段用来描述修改了Ref值的事务的相关信息,包括事务启动的相对顺序以及当前的状态.借助这个字段当前事务知道还有哪些事务修改了Ref还没有提交.这个字段的使用可以参看LockingTransaction类的lock方法.

 

//下面是tinfo的结构

https://github.com/clojure/clojure/blob/master/src/jvm/clojure/lang/LockingTransaction.java

public static class Info{
     final AtomicInteger status;
     final long startPoint;
     final CountDownLatch latch;


     public Info(int status, long startPoint){
          this.status = new AtomicInteger(status);
          this.startPoint = startPoint;
          this.latch = new CountDownLatch(1);
     }

     public boolean running(){
          int s = status.get();
          return s == RUNNING || s == COMMITTING;
     }
}


//returns the most recent val
Object lock(Ref ref){
     //can't upgrade readLock, so release it
     releaseIfEnsured(ref);

     boolean unlocked = true;
     try
          {
          tryWriteLock(ref);
          unlocked = false;

          if(ref.tvals != null && ref.tvals.point > readPoint)
               throw retryex;
          Info refinfo = ref.tinfo;

          //write lock conflict
          if(refinfo != null && refinfo != info && refinfo.running())
               {
               if(!barge(refinfo))
                    {
                    ref.lock.writeLock().unlock();
                    unlocked = true;
                    return blockAndBail(refinfo);
                    }
               }
          ref.tinfo = info;
          return ref.tvals == null ? null : ref.tvals.val;
          }
     finally
          {
          if(!unlocked)
               ref.lock.writeLock().unlock();
          }
}

 

 

In-transaction Values



    每个LockingTransaction对象都在vals:HashMap<Ref, Object>字段维护了事务中修改过的Ref的in-transaction值.如果Ref是在事务中只是读操作,那么它的值总是从tvals的committed value chain中获取,沿着committed value chain找到当前事务启动前最新的值.如果事务进行多次Ref的deref操作那么,这个寻找合适值的过程就要执行多次,但这种开销除非是Ref值真的改动了才是必要的.Ref值第一次在事务中修改其新值就会放在vals Map中,在后续的事务过程中,值就只需要在vals map中获取即可.

 

 

Faults



    下面的情况会导致"Fault"出现:在事务内部读取Ref的值,但是Ref并没有in-transaction值而且当前事务开始之后Comitted value chain才有值.当前事务Try开始之后别的事务才提交行为就可能导致这种情况.这段逻辑看下面的代码:

Object doGet(Ref ref){
     if(!info.running())
          throw retryex;
     if(vals.containsKey(ref))
          return vals.get(ref);
     try
          {
          ref.lock.readLock().lock();
          if(ref.tvals == null)
               throw new IllegalStateException(ref.toString() + " is unbound.");
          Ref.TVal ver = ref.tvals;
          do
               {
               if(ver.point <= readPoint)
                    return ver.val;
               } while((ver = ver.prior) != ref.tvals);
          }
     finally
          {
          ref.lock.readLock().unlock();
          }
     //no version of val precedes the read point
     ref.faults.incrementAndGet();
     throw retryex;

}

 

   Fault出现,事务就会进行retry.如果事务提交变动的Ref在别的事务中已经经历了一次Fault而且Committed Value Chain的长度小于maxHsitory,就会在Ref的Committed添加一个值节点.chain长度小于minhistory的时候也会触发值节点添加. 度量chain的长度减小了在这个Ref上再出现fault的可能性.如果Ref上没有发生fault,就不是添加一个新的node,chain的最后一个node变成第一个"最新提交的值".

每一个Ref的history chain长度可以不同,Chain长度随着fault的发生自动适应.如果一个Ref上从不出现fault那么它的minhistory值是0(默认值) chain的长度不会超过1. Chain从来不会收缩,falut会导致chain变长,它要么保持长度要么随着fault出现增长.

   //at this point, all values calced, all refs to be written locked
                    //no more client code to be called
                    long msecs = System.currentTimeMillis();
                    long commitPoint = getCommitPoint();
                    for(Map.Entry<Ref, Object> e : vals.entrySet())
                         {
                         Ref ref = e.getKey();
                         Object oldval = ref.tvals == null ? null : ref.tvals.val;
                         Object newval = e.getValue();
                         int hcount = ref.histCount();

                         if(ref.tvals == null)
                              {
                              ref.tvals = new Ref.TVal(newval, commitPoint, msecs);
                              }
                         else if((ref.faults.get() > 0 && hcount < ref.maxHistory)
                                   || hcount < ref.minHistory)
                              {
                              ref.tvals = new Ref.TVal(newval, commitPoint, msecs, ref.tvals);
                              ref.faults.set(0);
                              }
                         else
                              {
                              ref.tvals = ref.tvals.next;
                              ref.tvals.val = newval;
                              ref.tvals.point = commitPoint;
                              ref.tvals.msecs = msecs;
                              }
                         if(ref.getWatches().count() > 0)
                              notify.add(new Notify(ref, oldval, newval));
                         }



Barging



事务B正在做重试,判断事务A是否可以继续继续执行的决策过程称为"抢先"(barge).一个事务想抢先于另一个事务需要满足三个条件:

* 事务A 必须运行了至少1/100秒 (BARGE_WAIT_NANOS)
* 事务A 比事务B启动要早 (偏爱older事务)
* 事务B 状态是Running可以转换到KILLED的状态,不会打断正在提交的事务

如果不满足上述条件,事务A重试,事务B继续执行.

public static final long BARGE_WAIT_NANOS = 10 * 1000000;

private boolean bargeTimeElapsed(){
     return System.nanoTime() - startTime > BARGE_WAIT_NANOS;
}

private boolean barge(Info refinfo){
     boolean barged = false;
     //if this transaction is older
     //  try to abort the other
     if(bargeTimeElapsed() && startPoint < refinfo.startPoint)
          {
        barged = refinfo.status.compareAndSet(RUNNING, KILLED);
        if(barged)
            refinfo.latch.countDown();
          }
     return barged;
}

 

Retries

 事务进行重试时,之前Ref的所有in-transaction变动都会被舍弃,并跳转回事务体的起始位置.有很多原因导致重试:

1. 在Ref上调用ref-set或alter会"锁住"Ref,但出现下面的情况:
(a) 其它线程获取了Ref的读锁或写锁,导致无法获取Ref的写锁.
(b) 在当前事务开始之后,已经有Ref的变动完成了提交.
(c) 另外一个事务已经做了in-transaction的变动还没有提交,尝试抢先(barge)于其它事务提交失败


2. 尝试在事务中读取Ref的值,但出现了下面的情况:
(a) 别的事务抢先(barge)当前事务提交(当前事务的状态不是RUNNING)
(b) 出现Fault.


3. 在Ref上调用 ref-set , alter , commute , ensure,但当前事务(当前事务状态不是Running)被其它事务抢先(barge)提交.
4. 当前事务正在提交变动(committing),但另一个正在运行(running)的事务有事务中修改,尝试抢先于该进程提交失败.


事务不会无休止的进行重试,LockingTransaction定义了重试的上限:public static final int RETRY_LIMIT = 10000;超过限制会抛出异常.
重试的触发是通过抛出RetryEx实现的.注意它是java.lang.Error的子类而不是Exception,这样就不会被开发者吞掉异常.retry循环中有RetryEX异常捕获处理逻辑.catch之后逻辑回到loop的起始位置,由于并没有捕获其它异常,所以一旦有其它异常产生就会导致退出重试循环.

static class RetryEx extends Error{
}

  // 看一下简单的代码结构:
Object run(Callable fn) throws Exception{
     boolean done = false;
     Object ret = null;
     ArrayList<Ref> locked = new ArrayList<Ref>();
     ArrayList<Notify> notify = new ArrayList<Notify>();

     for(int i = 0; !done && i < RETRY_LIMIT; i++)
          {
          try
               {
                 //. . . .
               }
          catch(RetryEx retry)
               {
               //eat this so we retry rather than fall out
               }
          finally
               {
               //. . .∓ .
               }
          }
     if(!done)
          throw Util.runtimeException("Transaction failed after reaching retry limit");
     return ret;
}

 

IllegalStateException



Clojure的很多方法都会抛出IllegalStateException.如果在事务内部抛出这个异常,事务不会进行Retry.STM中会抛出这个异常的情况有:


* 尝试在当前线程获取LockingTransaction对象,但是该对象还没有创建或事务并没有运行. 在事务外调用下面的方法就会看到这个异常:ref-set ,alter , commute or ensure .(后面有测试代码)
* 尝试获取Ref的值 但是还没有绑定值
* 尝试使用ref-set或alter修改Ref的in-transaction值 但commute函数已经在当前Ref上调用.
* Ref的validation方法返回false或者抛出异常

 
Clojure 1.4.0
user=> (def a (ref 101))
#'user/a
user=> (ref-set a 1024)
IllegalStateException No transaction running  clojure.lang.LockingTransaction.getEx (LockingTransaction.java:208)
user=>
user=>  (def b (ref 23))
#'user/b
user=> @b
23
user=>  (dotimes [i 5] (dosync  (alter b + (commute b + 1000) )))
IllegalStateException Can't set after commute  clojure.lang.LockingTransaction.d
oSet (LockingTransaction.java:420)
user=>

 

Deadlocks, Livelocks and Race Conditions



   Clojure STM的实现保证了deadlocks, livelocks 和 race conditions不会出现.

   在多线程应用的语境中,上面这些术语可以这样描述:deadlock 发生在并发运行的线程互相等待其它进程独占的资源. livelock 发生在总是无法获得完成任务需要的资源永远等待的情况.race condition 多线程环修改数据,最后结果取决于运行时的精确时序.

   Clojure STM中Ref的修改是在事务中执行的.假设事务A B依次启动并发执行.如果二者都想使用ref-set alter修改同一个Ref(写冲突),这种情况会优先让事务A执行,理由是它已经运行了较长的时间(假设它的运行时间已经超过了BARGE_WAIT_NANOS).如果是事务修改Ref的顺序决定了重试的执行顺序就可能带来活锁(livelock).事务 B会进行重试,可能重试多次,可能在事务A提交之后就看完成对Ref的修改了.事务B最终运行完成都是使用事务A提交的一致的状态视图.

如果事务使用commute修改Ref,不会有写冲突. 获取Refs的写锁是按照Ref Objects的创建顺序依次获取的.

STM Overhead


    使用STM比显示用锁还是增加一些开销的,但这并不表示STM更慢.STM是乐观锁,显示锁是悲观锁,整体上STM能够提供更好的性能和更大程度上的并发.当一个Ref在事务内还没有被修改的时候会从其committed value chain中选择合适的值.只要从当前事务开始起没有事务提交新的值,这个选择过程还是非常快的,因为值就是committed value chain的第一个节点.否则就要进行遍历.

Ref的值只能在事务内部修改,这就有一个检验事务是否正在运行的额外成本.还要检查Ref上没有调用commute方法. 这玩意没有意思 因为commute函数会在提交阶段再次调用 在事务完成后 其它对Ref的赋值不会有持久的影响.如果这是Ref在事务中首次被修改,还要添加Ref到当前事务修改过的Ref集合中,标记Ref被当前事务修改(后面章节提到的lock会有详细描述). 最后,新值被添加到当前事务的in-transaction values的map中.后续的读操作就会从map中读而非遍历committed values chain.看下面的代码:

 

Object doSet(Ref ref, Object val){
     if(!info.running())
          throw retryex;
     if(commutes.containsKey(ref))
          throw new IllegalStateException("Can't set after commute");
     if(!sets.contains(ref))
          {
          sets.add(ref);
          lock(ref);
          }
     vals.put(ref, val);
     return val;
}

 

LockingTransaction类的lock方法会引入一些开销,总结其操作步骤:

1. 如果在当前transaction try较早时机在Ref上调用了ensure那么就要释放Ref的读锁
2. 尝试获取Ref写锁 如果获取失败就触发当前事务的retry
3. 如果当前transaction try开始之后另外的事务已经提交了对Ref的修改触发retry
4. 如果另外的事务对Ref有一个尚未提交的更改,企图抢先barge.如果失败就触发当前事务的retry
5. ref.tinfo = info; 把当前的Ref标记为被当前事务锁定
6. 无论如何都释放Ref的写锁

贴出来代码对照着看:

//returns the most recent val
Object lock(Ref ref){
     //can't upgrade readLock, so release it
     releaseIfEnsured(ref);

     boolean unlocked = true;
     try
          {
          tryWriteLock(ref);
          unlocked = false;
          //第三条说的开销
          if(ref.tvals != null && ref.tvals.point > readPoint)
               throw retryex;
          Info refinfo = ref.tinfo;

          //write lock conflict
          if(refinfo != null && refinfo != info && refinfo.running())
               {
               if(!barge(refinfo))
                    {
                    ref.lock.writeLock().unlock();
                    unlocked = true;
                    return blockAndBail(refinfo);
                    }
               }
          ref.tinfo = info;
          return ref.tvals == null ? null : ref.tvals.val;
          }
     finally
          {
          if(!unlocked)
               ref.lock.writeLock().unlock();
          }
}
// 这就是第一条所说的
private void releaseIfEnsured(Ref ref){
     if(ensures.contains(ref))
          {
          ensures.remove(ref);
          ref.lock.readLock().unlock();
          }
}
//这就是第二条说的  尝试获取写锁 如果失败就抛出retryex
void tryWriteLock(Ref ref){
     try
          {
          if(!ref.lock.writeLock().tryLock(LOCK_WAIT_MSECS, TimeUnit.MILLISECONDS))
               throw retryex;
          }
     catch(InterruptedException e)
          {
          throw retryex;
          }
}

 


事务结束的时候要,提交Ref的变动还有一些开销,提交之后新的变动外部可见.需要的步骤如下:

1. 修改当前事务的状态RUNNING -> COMMITTING
2. 再次运行事务中的commute方法.这需要在每一个commuted Ref上获取写锁
3. 获取在事务中修改过Ref的写锁
4. 在所有被修改过的Ref上调用Validator函数,如果出现验证失败的情况就触发当前事务的Retry.
5. 对于改动过的Ref添加node到committed value chain或修改现有node,如何操作取决于falut minhistory maxhistory
6. 对于被改动过且至少有一个watcher的Ref创建Notify对象
7. 修改事务状态COMMITTING -> COMMITTED
8. 释放之前获取的所有写锁
9. 释放所有因为ensure调用而持有的读锁
10. 清理现场 locked (local), ensures , vals , sets and commutes为下一次Retry或者当前线程的新事务做准备
11. 如果提交成功,通知所有注册了变动watcher使用Notify对象 并且将事务中的所有所有的action分发到Agents
12. 清理notify (local) 和actions collections 为下一次Retry或当前线程的下一个事务做准备
13. 返回事务体(transaction body)的最后一个表达式的值


看代码片段:

    //make sure no one has killed us before this point, and can't from now on
               if(info.status.compareAndSet(RUNNING, COMMITTING))
                    {
                    for(Map.Entry<Ref, ArrayList<CFn>> e : commutes.entrySet())
                         {
                         Ref ref = e.getKey();
                         if(sets.contains(ref)) continue;

                         boolean wasEnsured = ensures.contains(ref);
                         //can't upgrade readLock, so release it
                         releaseIfEnsured(ref);
                         tryWriteLock(ref);
                         locked.add(ref);
                         if(wasEnsured && ref.tvals != null && ref.tvals.point > readPoint)
                              throw retryex;

                         Info refinfo = ref.tinfo;
                         if(refinfo != null && refinfo != info && refinfo.running())
                              {
                              if(!barge(refinfo))
                                   throw retryex;
                              }
                         Object val = ref.tvals == null ? null : ref.tvals.val;
                         vals.put(ref, val);
                         for(CFn f : e.getValue())
                              {
                              vals.put(ref, f.fn.applyTo(RT.cons(vals.get(ref), f.args)));
                              }
                         }
                    for(Ref ref : sets)
                         {
                         tryWriteLock(ref);
                         locked.add(ref);
                         }

                    //validate and enqueue notifications
                    for(Map.Entry<Ref, Object> e : vals.entrySet())
                         {
                         Ref ref = e.getKey();
                         ref.validate(ref.getValidator(), e.getValue());
                         }

                    //at this point, all values calced, all refs to be written locked
                    //no more client code to be called
                    long msecs = System.currentTimeMillis();
                    long commitPoint = getCommitPoint();
                    for(Map.Entry<Ref, Object> e : vals.entrySet())
                         {
                         Ref ref = e.getKey();
                         Object oldval = ref.tvals == null ? null : ref.tvals.val;
                         Object newval = e.getValue();
                         int hcount = ref.histCount();

                         if(ref.tvals == null)
                              {
                              ref.tvals = new Ref.TVal(newval, commitPoint, msecs);
                              }
                         else if((ref.faults.get() > 0 && hcount < ref.maxHistory)
                                   || hcount < ref.minHistory)
                              {
                              ref.tvals = new Ref.TVal(newval, commitPoint, msecs, ref.tvals);
                              ref.faults.set(0);
                              }
                         else
                              {
                              ref.tvals = ref.tvals.next;
                              ref.tvals.val = newval;
                              ref.tvals.point = commitPoint;
                              ref.tvals.msecs = msecs;
                              }
                         if(ref.getWatches().count() > 0)
                              notify.add(new Notify(ref, oldval, newval));
                         }

                    done = true;
                    info.status.set(COMMITTED);
                    }
                  }
          catch(RetryEx retry)
               {
               //eat this so we retry rather than fall out
               }
          finally
               {
               for(int k = locked.size() - 1; k >= 0; --k)
                    {
                    locked.get(k).lock.writeLock().unlock();
                    }
               locked.clear();
               for(Ref r : ensures)
                    {
                    r.lock.readLock().unlock();
                    }
               ensures.clear();
               stop(done ? COMMITTED : RETRY);
               try
                    {
                    if(done) //re-dispatch out of transaction
                         {
                         for(Notify n : notify)
                              {
                              n.ref.notifyWatches(n.oldval, n.newval);
                              }
                         for(Agent.Action action : actions)
                              {
                              Agent.dispatchAction(action);
                              }
                         }
                    }
               finally
                    {
                    notify.clear();
                    actions.clear();
                    }
               }
          }
     if(!done)
          throw Util.runtimeException("Transaction failed after reaching retry limit");
     return ret;
}


https://github.com/clojure/clojure/blob/master/src/jvm/clojure/lang/ARef.java

void validate(IFn vf, Object val){
     try
          {
          if(vf != null && !RT.booleanCast(vf.invoke(val)))
               throw new IllegalStateException("Invalid reference state");
          }
     catch(RuntimeException re)
          {
          throw re;
          }
     catch(Exception e)
          {
          throw new IllegalStateException("Invalid reference state", e);
          }
}

 

 明确了上面的概念代码实现,之前的疑惑已经能解答了,对Clojure STM也有了一点认识,好奇心远远未满足,会继续关注.



附原文引用:

 

 最后,小图一张  祝大家新年 嘉欣