下班后记一些有趣的东西。
这个话题是阿宝同学问我为什么clojure的PersistentVector的节点Node为什么要有个原子引用指向一个线程:
我还真不懂,没有细看过这部分代码,早上花点时间学习了下。
PersistentVector的实现是另一个话题,这里不提。我们都知道clojure的数据结构是immutable的,修改任意一个数据结构都将生成一个新的数据结构,原来的不变。为了减少复制的开销,clojure的数据结构同时是persistent,所谓持久数据结构是将数据组织为树形的层次结构,修改的时候只是root改变,指向不同的节点,原有的节点仍然复用,从而避免了大量的数据复制,具体可以搜索下 ideal hash trees这篇paper, paper难懂,可以看看 这篇blog。
但是在创建PersistentVector的时候,从一堆现有的元素或者集合创建一个PersistentVector,如果每次都重新生成一个PersistentVector,未免太浪费,创建过程的性能会受到影响。我们完全可以假设创建PersistentVector这个过程肯定是线程安全的,没有必要每添加一个元素就重新生成一个PersistentVector,完全可以在同一个PersistentVector上修改。这就是TransientVector的意义所在。
TransientVector就是一个可修改的Vector,调用它添加一个元素,删除一个元素,都是在同一个对象上进行,而不是生成新的对象。查看PersistentVector的创建:
static public PersistentVector create(ISeq items){
TransientVector ret = EMPTY.asTransient();
for (; items != null ; items = items.next())
ret = ret.conj(items.first());
return ret.persistent();
}
static public PersistentVector create(List items){
TransientVector ret = EMPTY.asTransient();
for (Object item : items)
ret = ret.conj(item);
return ret.persistent();
}
static public PersistentVector create(Object items){
TransientVector ret = EMPTY.asTransient();
for (Object item : items)
ret = ret.conj(item);
return ret.persistent();
}
看到三个方法的第一步都是将EMPTY集合transient化,生成一个可修改的TransientVector:
生成的时候记录了当前的线程在root节点。然后添加元素的时候直接调用TransientVector的conj方法,查看conj可以看到每次返回的都是this:
查看ensureEditable方法:
终于看到Node中的edit引用的线程被使用了,判断当前修改的线程是否是使得集合transient化的线程,如果不是则抛出异常,这是为了保证对TransientVector的编辑是在同一个线程里,防止因为意外发布TransientVector引用引起的线程安全隐患。
知道了transient集合的用途,我们能在clojure中使用吗?完全没问题,clojure.core有个transient方法,可以将一个集合transient化:
前提是这个集合是可编辑的,clojure的map、vector和set都是可编辑的。让我们确认下transient修改后的集合还是不是自身:
定义了集合v1,v2是调用了transient之后的集合,查看v2,果然是一个 TransientVector。查看v2的元素个数是不是3个:
没问题,注意,我们不能直接调用count函数,因为v2是个普通的java对象,我们必须使用dot操作符来调用java对象的方法。添加一个元素看看:
添加一个元素后形成集合v3,查看v3,跟v2是同一个对象 # < TransientVector clojure.lang.PersistentVector$TransientVector@7eb366 >
。证明了transient集合修改的是自身,而不是生成一个新集合。确认下4有加入v2和v3:
果然没有问题。transient集合的使用应当慎重,除非能确认没有其他线程会去修改集合,并且对线程的可见性要求不高的时候,也许可以尝试下这个技巧。
这个话题是阿宝同学问我为什么clojure的PersistentVector的节点Node为什么要有个原子引用指向一个线程:
static
class
Node
implements
Serializable {
// 记录的线程
transient final AtomicReference < Thread > edit;
final Object[] array;
Node(AtomicReference < Thread > edit, Object[] array){
this .edit = edit;
this .array = array;
}
Node(AtomicReference < Thread > edit){
this .edit = edit;
this .array = new Object[ 32 ];
}
}
// 记录的线程
transient final AtomicReference < Thread > edit;
final Object[] array;
Node(AtomicReference < Thread > edit, Object[] array){
this .edit = edit;
this .array = array;
}
Node(AtomicReference < Thread > edit){
this .edit = edit;
this .array = new Object[ 32 ];
}
}
我还真不懂,没有细看过这部分代码,早上花点时间学习了下。
PersistentVector的实现是另一个话题,这里不提。我们都知道clojure的数据结构是immutable的,修改任意一个数据结构都将生成一个新的数据结构,原来的不变。为了减少复制的开销,clojure的数据结构同时是persistent,所谓持久数据结构是将数据组织为树形的层次结构,修改的时候只是root改变,指向不同的节点,原有的节点仍然复用,从而避免了大量的数据复制,具体可以搜索下 ideal hash trees这篇paper, paper难懂,可以看看 这篇blog。
但是在创建PersistentVector的时候,从一堆现有的元素或者集合创建一个PersistentVector,如果每次都重新生成一个PersistentVector,未免太浪费,创建过程的性能会受到影响。我们完全可以假设创建PersistentVector这个过程肯定是线程安全的,没有必要每添加一个元素就重新生成一个PersistentVector,完全可以在同一个PersistentVector上修改。这就是TransientVector的意义所在。
TransientVector就是一个可修改的Vector,调用它添加一个元素,删除一个元素,都是在同一个对象上进行,而不是生成新的对象。查看PersistentVector的创建:
static public PersistentVector create(ISeq items){
TransientVector ret = EMPTY.asTransient();
for (; items != null ; items = items.next())
ret = ret.conj(items.first());
return ret.persistent();
}
static public PersistentVector create(List items){
TransientVector ret = EMPTY.asTransient();
for (Object item : items)
ret = ret.conj(item);
return ret.persistent();
}
static public PersistentVector create(Object items){
TransientVector ret = EMPTY.asTransient();
for (Object item : items)
ret = ret.conj(item);
return ret.persistent();
}
看到三个方法的第一步都是将EMPTY集合transient化,生成一个可修改的TransientVector:
TransientVector(PersistentVector v){
this (v.cnt, v.shift, editableRoot(v.root), editableTail(v.tail));
}
static Node editableRoot(Node node){
return new Node( new AtomicReference < Thread > (Thread.currentThread()), node.array.clone());
}
this (v.cnt, v.shift, editableRoot(v.root), editableTail(v.tail));
}
static Node editableRoot(Node node){
return new Node( new AtomicReference < Thread > (Thread.currentThread()), node.array.clone());
}
生成的时候记录了当前的线程在root节点。然后添加元素的时候直接调用TransientVector的conj方法,查看conj可以看到每次返回的都是this:
public
TransientVector conj(Object val){
// 确保修改过程合法
ensureEditable();
// 忽略逻辑
return this ;
}
// 确保修改过程合法
ensureEditable();
// 忽略逻辑
return this ;
}
查看ensureEditable方法:
void
ensureEditable(){
Thread owner = root.edit.get();
if (owner == Thread.currentThread())
return ;
if (owner != null )
throw new IllegalAccessError( " Transient used by non-owner thread " );
throw new IllegalAccessError( " Transient used after persistent! call " );
}
Thread owner = root.edit.get();
if (owner == Thread.currentThread())
return ;
if (owner != null )
throw new IllegalAccessError( " Transient used by non-owner thread " );
throw new IllegalAccessError( " Transient used after persistent! call " );
}
终于看到Node中的edit引用的线程被使用了,判断当前修改的线程是否是使得集合transient化的线程,如果不是则抛出异常,这是为了保证对TransientVector的编辑是在同一个线程里,防止因为意外发布TransientVector引用引起的线程安全隐患。
知道了transient集合的用途,我们能在clojure中使用吗?完全没问题,clojure.core有个transient方法,可以将一个集合transient化:
(defn
transient
[ ^ clojure.lang.IEditableCollection coll]
(.asTransient coll))
[ ^ clojure.lang.IEditableCollection coll]
(.asTransient coll))
前提是这个集合是可编辑的,clojure的map、vector和set都是可编辑的。让我们确认下transient修改后的集合还是不是自身:
user
=>
(def v1 [
1
2
3
])
# ' user/v1
user => (def v2 ( transient v1))
# ' user/v2
user => v2
# < TransientVector clojure.lang.PersistentVector$TransientVector@7eb366 >
# ' user/v1
user => (def v2 ( transient v1))
# ' user/v2
user => v2
# < TransientVector clojure.lang.PersistentVector$TransientVector@7eb366 >
定义了集合v1,v2是调用了transient之后的集合,查看v2,果然是一个 TransientVector。查看v2的元素个数是不是3个:
user
=>
(.count v2)
3
3
没问题,注意,我们不能直接调用count函数,因为v2是个普通的java对象,我们必须使用dot操作符来调用java对象的方法。添加一个元素看看:
user
=>
(def v3 (.conj v2
4
))
# ' user/v3
user => v3
# < TransientVector clojure.lang.PersistentVector$TransientVector@7eb366 >
# ' user/v3
user => v3
# < TransientVector clojure.lang.PersistentVector$TransientVector@7eb366 >
添加一个元素后形成集合v3,查看v3,跟v2是同一个对象 # < TransientVector clojure.lang.PersistentVector$TransientVector@7eb366 >
。证明了transient集合修改的是自身,而不是生成一个新集合。确认下4有加入v2和v3:
user
=>
(.nth v3
3
)
4
user => (.count v2)
4
user => (.count v3)
4
user => (.nth v2 3 )
4
4
user => (.count v2)
4
user => (.count v3)
4
user => (.nth v2 3 )
4
果然没有问题。transient集合的使用应当慎重,除非能确认没有其他线程会去修改集合,并且对线程的可见性要求不高的时候,也许可以尝试下这个技巧。