Java 写时复制容器 —— CopyOnWriteArrayList
引言
寫時復制的含義是當容器發生修改操作時,如add()?等,就會將原來的容器整體復制一份,這個過程是加鎖的。而如果只是讀取資源,例如 get() ,就不會受到任何同步要求的限制。
寫時復制的理念是,如果多個讀取線程請求相同的數據,它們會共享相同的數據,而不需要考慮并發修改的問題不得不在線程內部生成一份數據副本;當容器發生修改操作時,系統這時才會真正復制一個副本給其他請求者,也就是說,寫時復制的理念最主要是解決訪問者需要考慮并發修改的問題,有了這種機制,就可以避免生成線程副本或加鎖訪問,只要容器沒有被修改,就不會產生任何數據副本。在很大程度上提高了讀的性能。
但缺點顯而易見,如果容器依然有大量的修改操作,或讀寫比不高的話,使用寫時復制容器只會降低程序性能。
一、CopyOnWriteArrayList
1.1 寫入效率
public class T04_CopyOnWriteList {public static void runAndComputeTime(Thread[] ths) {long s1 = System.currentTimeMillis();Arrays.stream(ths).forEach(t -> t.start());Arrays.stream(ths).forEach(t -> {try {t.join();} catch (InterruptedException e) {e.printStackTrace();}});long s2 = System.currentTimeMillis();System.out.println(s2 - s1 + " ms");}public static void main(String[] args) { // List<String> list = new CopyOnWriteArrayList<>(); // output:7565 ms // List<String> list = new Vector<>(); // output:53 msList<String> list = Collections.synchronizedList(new ArrayList<>()); // output:52 msThread[] ths = new Thread[100];for (int i = 0; i < ths.length; i++) {Runnable task = () -> {for (int j = 0; j < 1000; j++) {list.add("a" + j);}};ths[i] = new Thread(task);}runAndComputeTime(ths);System.out.println(list.size());} }上述代碼模擬了100個線程,每個線程向 list 中加入1000個字符串的操作。
runAndComputeTime() 方法先是啟動線程,然后通過 join 方法,依次將所有線程合并到主線程中,這么做的目的主要是讓全部線程的執行時間累加,從而得出一個總時間。
最后輸出消耗時間和 list 存儲數量,消耗時間不必多說, list 存儲數量主要是看并發場景下線程安全性,不能出現“丟失數據”的情況,想一想 如果用 ArrayList,最終 size() 方法輸出多少?
從輸出結果來看,兩個同步容器執行效率相當,都是 50 ~ 80 ms 左右,而CopyOnWriteArrayList 執行效率最長,達到了驚人的 7500 ms。
所以,如果不是確定寫入操作極少,就一定不要使用 CopyOnWriteArrayList!
1.2 讀取效率
public class T05_CopyOnWriteList {private static List<String> list = new CopyOnWriteArrayList<>(); // output:66ms // private static List<String> list = new Vector<>();// output:956ms // private static List<String> list = Collections.synchronizedList(new ArrayList<>());// output:873msstatic {for (int i = 0; i < 100000; i++) {list.add(String.valueOf(i));}}public static void main(String[] args) {Thread[] ths = new Thread[100];for (int i = 0; i < ths.length; i++) {Runnable task = () -> {for (int j = 0; j < list.size(); j++) {list.get(j);}};ths[i] = new Thread(task);}System.out.println("開始...");long t1 = System.currentTimeMillis();Arrays.stream(ths).forEach(t -> t.start());Arrays.stream(ths).forEach(t -> {try {t.join();} catch (InterruptedException e) {e.printStackTrace();}});long t2 = System.currentTimeMillis();System.out.println(t2 - t1 + "ms");} }上述代碼中,先通過 static 塊初始化了一個 list,然后通過 100個線程并發讀取 list 中的數據,最后通過 join 累加所有過程的執行時間,并輸出消耗時間。
從執行結果來看,同步容器的執行效率在 800~900ms 左右,而 CopyOnWriteArrayList 可以達到驚人的 百毫秒以內,效率提升了十倍以上。
所以,如果確定一批數據的寫入操作極少,而讀取操作非常頻繁的話,可以考慮使用 CopyOnWriteArrayList 容器,線程安全+讀性能可觀。
二、CopyOnWriteArrayList 源碼分析
寫時復制容器的寫入操作包括 add 、set 等,實現邏輯幾乎完全一致,以 add() 為例:
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();}}容器內部維護了一個 ReentrantLock 作為鎖的實現,在執行 add 操作時,先進行鎖定。
然后取得底層數組,并拷貝一個長度 +1 的新數組,并將新元素放入最后。
將容器指針指向新的數組后,unlock 解鎖。
由此可見,寫入慢的原因就不言自明了,加鎖、整個數組拷貝,這兩個邏輯就是寫入慢的真正的元兇。
我們再來看下讀取的邏輯,以 get() 為例:
public E get(int index) {return get(getArray(), index); }@SuppressWarnings("unchecked") private E get(Object[] a, int index) {return (E) a[index]; }整個獲取元素的過程未加任何鎖,原因就是容器已經在修改的時候保證了同步邏輯,極大的提升了讀取的效率。
總結
寫時復制的觀念就是在修改時復制容器的副本,從而避免在讀取時需要考慮額外的并發修改問題。
寫時復制容器的應用場景是寫入操作極少,讀取操作非常多的情況,切不可在包含大量寫入操作的場景下使用 CopyOnWrite 。
Java 的寫時復制容器實現是 CopyOnWriteArrayList,其底層就是一個定長數組,當容器發生修改時,會使用容器內的 ReentrantLock 上鎖,并拷貝整個數組完成操作。
當發生讀取時,可以像單線程那樣不需要加任何同步機制,可以讓多線程并發讀取的效率達到最大。
總結
以上是生活随笔為你收集整理的Java 写时复制容器 —— CopyOnWriteArrayList的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: MySQL 基础 ————事务与隔离级别
- 下一篇: java 多态判断非空_Java 多态