Java 写时复制容器 —— CopyOnWriteArrayList
引言
寫時復(fù)制的含義是當(dāng)容器發(fā)生修改操作時,如add()?等,就會將原來的容器整體復(fù)制一份,這個過程是加鎖的。而如果只是讀取資源,例如 get() ,就不會受到任何同步要求的限制。
寫時復(fù)制的理念是,如果多個讀取線程請求相同的數(shù)據(jù),它們會共享相同的數(shù)據(jù),而不需要考慮并發(fā)修改的問題不得不在線程內(nèi)部生成一份數(shù)據(jù)副本;當(dāng)容器發(fā)生修改操作時,系統(tǒng)這時才會真正復(fù)制一個副本給其他請求者,也就是說,寫時復(fù)制的理念最主要是解決訪問者需要考慮并發(fā)修改的問題,有了這種機(jī)制,就可以避免生成線程副本或加鎖訪問,只要容器沒有被修改,就不會產(chǎn)生任何數(shù)據(jù)副本。在很大程度上提高了讀的性能。
但缺點(diǎn)顯而易見,如果容器依然有大量的修改操作,或讀寫比不高的話,使用寫時復(fù)制容器只會降低程序性能。
一、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 方法,依次將所有線程合并到主線程中,這么做的目的主要是讓全部線程的執(zhí)行時間累加,從而得出一個總時間。
最后輸出消耗時間和 list 存儲數(shù)量,消耗時間不必多說, list 存儲數(shù)量主要是看并發(fā)場景下線程安全性,不能出現(xiàn)“丟失數(shù)據(jù)”的情況,想一想 如果用 ArrayList,最終 size() 方法輸出多少?
從輸出結(jié)果來看,兩個同步容器執(zhí)行效率相當(dāng),都是 50 ~ 80 ms 左右,而CopyOnWriteArrayList 執(zhí)行效率最長,達(dá)到了驚人的 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個線程并發(fā)讀取 list 中的數(shù)據(jù),最后通過 join 累加所有過程的執(zhí)行時間,并輸出消耗時間。
從執(zhí)行結(jié)果來看,同步容器的執(zhí)行效率在 800~900ms 左右,而 CopyOnWriteArrayList 可以達(dá)到驚人的 百毫秒以內(nèi),效率提升了十倍以上。
所以,如果確定一批數(shù)據(jù)的寫入操作極少,而讀取操作非常頻繁的話,可以考慮使用 CopyOnWriteArrayList 容器,線程安全+讀性能可觀。
二、CopyOnWriteArrayList 源碼分析
寫時復(fù)制容器的寫入操作包括 add 、set 等,實(shí)現(xiàn)邏輯幾乎完全一致,以 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();}}容器內(nèi)部維護(hù)了一個 ReentrantLock 作為鎖的實(shí)現(xiàn),在執(zhí)行 add 操作時,先進(jìn)行鎖定。
然后取得底層數(shù)組,并拷貝一個長度 +1 的新數(shù)組,并將新元素放入最后。
將容器指針指向新的數(shù)組后,unlock 解鎖。
由此可見,寫入慢的原因就不言自明了,加鎖、整個數(shù)組拷貝,這兩個邏輯就是寫入慢的真正的元兇。
我們再來看下讀取的邏輯,以 get() 為例:
public E get(int index) {return get(getArray(), index); }@SuppressWarnings("unchecked") private E get(Object[] a, int index) {return (E) a[index]; }整個獲取元素的過程未加任何鎖,原因就是容器已經(jīng)在修改的時候保證了同步邏輯,極大的提升了讀取的效率。
總結(jié)
寫時復(fù)制的觀念就是在修改時復(fù)制容器的副本,從而避免在讀取時需要考慮額外的并發(fā)修改問題。
寫時復(fù)制容器的應(yīng)用場景是寫入操作極少,讀取操作非常多的情況,切不可在包含大量寫入操作的場景下使用 CopyOnWrite 。
Java 的寫時復(fù)制容器實(shí)現(xiàn)是 CopyOnWriteArrayList,其底層就是一個定長數(shù)組,當(dāng)容器發(fā)生修改時,會使用容器內(nèi)的 ReentrantLock 上鎖,并拷貝整個數(shù)組完成操作。
當(dāng)發(fā)生讀取時,可以像單線程那樣不需要加任何同步機(jī)制,可以讓多線程并發(fā)讀取的效率達(dá)到最大。
總結(jié)
以上是生活随笔為你收集整理的Java 写时复制容器 —— CopyOnWriteArrayList的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: MySQL 基础 ————事务与隔离级别
- 下一篇: w7系统计算机里没有摄像头,win7系统