日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) >

Java 写时复制容器 —— CopyOnWriteArrayList

發(fā)布時(shí)間:2025/3/12 38 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Java 写时复制容器 —— CopyOnWriteArrayList 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

引言

寫時(shí)復(fù)制的含義是當(dāng)容器發(fā)生修改操作時(shí),如add()?等,就會(huì)將原來(lái)的容器整體復(fù)制一份,這個(gè)過(guò)程是加鎖的。而如果只是讀取資源,例如 get() ,就不會(huì)受到任何同步要求的限制。

寫時(shí)復(fù)制的理念是,如果多個(gè)讀取線程請(qǐng)求相同的數(shù)據(jù),它們會(huì)共享相同的數(shù)據(jù),而不需要考慮并發(fā)修改的問(wèn)題不得不在線程內(nèi)部生成一份數(shù)據(jù)副本;當(dāng)容器發(fā)生修改操作時(shí),系統(tǒng)這時(shí)才會(huì)真正復(fù)制一個(gè)副本給其他請(qǐng)求者,也就是說(shuō),寫時(shí)復(fù)制的理念最主要是解決訪問(wèn)者需要考慮并發(fā)修改的問(wèn)題,有了這種機(jī)制,就可以避免生成線程副本或加鎖訪問(wèn),只要容器沒(méi)有被修改,就不會(huì)產(chǎn)生任何數(shù)據(jù)副本。在很大程度上提高了讀的性能。

但缺點(diǎn)顯而易見,如果容器依然有大量的修改操作,或讀寫比不高的話,使用寫時(shí)復(fù)制容器只會(huì)降低程序性能。

一、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個(gè)線程,每個(gè)線程向 list 中加入1000個(gè)字符串的操作。

runAndComputeTime() 方法先是啟動(dòng)線程,然后通過(guò) join 方法,依次將所有線程合并到主線程中,這么做的目的主要是讓全部線程的執(zhí)行時(shí)間累加,從而得出一個(gè)總時(shí)間。

最后輸出消耗時(shí)間和 list 存儲(chǔ)數(shù)量,消耗時(shí)間不必多說(shuō), list 存儲(chǔ)數(shù)量主要是看并發(fā)場(chǎng)景下線程安全性,不能出現(xiàn)“丟失數(shù)據(jù)”的情況,想一想 如果用 ArrayList,最終 size() 方法輸出多少?

從輸出結(jié)果來(lái)看,兩個(gè)同步容器執(zhí)行效率相當(dāng),都是 50 ~ 80 ms 左右,而CopyOnWriteArrayList 執(zhí)行效率最長(zhǎng),達(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");} }

上述代碼中,先通過(guò) static 塊初始化了一個(gè) list,然后通過(guò) 100個(gè)線程并發(fā)讀取 list 中的數(shù)據(jù),最后通過(guò) join 累加所有過(guò)程的執(zhí)行時(shí)間,并輸出消耗時(shí)間。

從執(zhí)行結(jié)果來(lái)看,同步容器的執(zhí)行效率在 800~900ms 左右,而 CopyOnWriteArrayList 可以達(dá)到驚人的 百毫秒以內(nèi),效率提升了十倍以上。

所以,如果確定一批數(shù)據(jù)的寫入操作極少,而讀取操作非常頻繁的話,可以考慮使用 CopyOnWriteArrayList 容器,線程安全+讀性能可觀。

二、CopyOnWriteArrayList 源碼分析

寫時(shí)復(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ù)了一個(gè) ReentrantLock 作為鎖的實(shí)現(xiàn),在執(zhí)行 add 操作時(shí),先進(jìn)行鎖定。

然后取得底層數(shù)組,并拷貝一個(gè)長(zhǎng)度 +1 的新數(shù)組,并將新元素放入最后。

將容器指針指向新的數(shù)組后,unlock 解鎖。

由此可見,寫入慢的原因就不言自明了,加鎖、整個(gè)數(shù)組拷貝,這兩個(gè)邏輯就是寫入慢的真正的元兇。

我們?cè)賮?lái)看下讀取的邏輯,以 get() 為例:

public E get(int index) {return get(getArray(), index); }@SuppressWarnings("unchecked") private E get(Object[] a, int index) {return (E) a[index]; }

整個(gè)獲取元素的過(guò)程未加任何鎖,原因就是容器已經(jīng)在修改的時(shí)候保證了同步邏輯,極大的提升了讀取的效率。

總結(jié)

寫時(shí)復(fù)制的觀念就是在修改時(shí)復(fù)制容器的副本,從而避免在讀取時(shí)需要考慮額外的并發(fā)修改問(wèn)題。

寫時(shí)復(fù)制容器的應(yīng)用場(chǎng)景是寫入操作極少,讀取操作非常多的情況,切不可在包含大量寫入操作的場(chǎng)景下使用 CopyOnWrite 。

Java 的寫時(shí)復(fù)制容器實(shí)現(xiàn)是 CopyOnWriteArrayList,其底層就是一個(gè)定長(zhǎng)數(shù)組,當(dāng)容器發(fā)生修改時(shí),會(huì)使用容器內(nèi)的 ReentrantLock 上鎖,并拷貝整個(gè)數(shù)組完成操作。

當(dāng)發(fā)生讀取時(shí),可以像單線程那樣不需要加任何同步機(jī)制,可以讓多線程并發(fā)讀取的效率達(dá)到最大。

總結(jié)

以上是生活随笔為你收集整理的Java 写时复制容器 —— CopyOnWriteArrayList的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。