下班后記一些有趣的東西。
這個話題是阿寶同學問我為什么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];
}
}
我還真不懂,沒有細看過這部分代碼,早上花點時間學習了下。
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());
}
生成的時候記錄了當前的線程在root節點。然后添加元素的時候直接調用TransientVector的conj方法,查看conj可以看到每次返回的都是this:
public TransientVector conj(Object val){
//確保修改過程合法
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");
}
終于看到Node中的edit引用的線程被使用了,判斷當前修改的線程是否是使得集合transient化的線程,如果不是則拋出異常,這是為了保證對TransientVector的編輯是在同一個線程里,防止因為意外發布TransientVector引用引起的線程安全隱患。
知道了transient集合的用途,我們能在clojure中使用嗎?完全沒問題,clojure.core有個transient方法,可以將一個集合transient化:
(defn transient
[^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>
定義了集合v1,v2是調用了transient之后的集合,查看v2,果然是一個
TransientVector。查看v2的元素個數是不是3個:
user=> (.count v2)
3
沒問題,注意,我們不能直接調用count函數,因為v2是個普通的java對象,我們必須使用dot操作符來調用java對象的方法。添加一個元素看看:
user=> (def v3 (.conj v2 4))
#'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
果然沒有問題。transient集合的使用應當慎重,除非能確認沒有其他線程會去修改集合,并且對線程的可見性要求不高的時候,也許可以嘗試下這個技巧。