寫時拷貝 Copy-On-Write

CopyOnWriteArrayList的內部也是一個數組,但這個數組是以原子方式被整體更新的。每次修改操作,都會新建一個數組,複製原數組的內容到新數組,在新數組上進行需要的修改,然後以原子方式設置內部的數組引用,這就是寫時拷貝。

private volatile transient Object[] array;final Object[] getArray() { return array; }final void setArray(Object[] a) { array = a;}transient final ReentrantLock lock = new ReentrantLock();public boolean add(E e) { final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); int len = elements.length; Object[] newElements = Arrays.copyOf(elements, len + 1); newElements[len] = e; setArray(newElements); return true; } finally { lock.unlock(); }}

每次修改都創建一個新數組,然後複製所有內容,這聽上去是一個難以令人接受的方案,如果數組比較大,修改操作又比較頻繁,可以想像,CopyOnWriteArrayList的性能是很低的。事實確實如此,CopyOnWriteArrayList不適用於數組很大,且修改頻繁的場景。它是以優化讀操作為目標的,讀不需要同步,性能很高,但在優化讀的同時就犧牲了寫的性能。

之前我們介紹了保證線程安全的兩種思路,一種是鎖,使用synchronized或ReentrantLock,另外一種是循環CAS寫時拷貝體現了保證線程安全的另一種思路。對於絕大部分訪問都是讀,且有大量並發線程要求讀,只有個別線程進行寫,且只是偶爾寫的場合,這種寫時拷貝就是一種很好的解決方案。

寫時拷貝是一種重要的思維,用於各種計算機程序中,比如經常用於操作系統內部的進程管理和內存管理。在進程管理中,子進程經常共享父進程的資源,只有在寫時在複製。在內存管理中,當多個程序同時訪問同一個文件時,操作系統在內存中可能只會載入一份,只有程序要寫時才會拷貝,分配自己的內存,拷貝可能也不會全部拷貝,而只會拷貝寫的位置所在的頁,頁是操作系統管理內存的一個單位,具體大小與系統有關,典型大小為4KB。

CopyOnWriteArraySet是基於CopyOnWriteArrayList實現的,與之HashSet/TreeSet相比,它的性能比較低,不適用於元素個數特別多的集合。如果元素個數比較多,可以考慮ConcurrentHashMap或ConcurrentSkipListSet.

private final CopyOnWriteArrayList<E> al;public CopyOnWriteArraySet() { al = new CopyOnWriteArrayList<E>();}public boolean add(E e) { return al.addIfAbsent(e);}

推薦閱讀:

Mutex and Spinlock

TAG:並發 | 數據結構 |