初識CopyOnWriteArrayList
第一次見到CopyOnWriteArrayList,是在研究JDBC的時(shí)候,每一個(gè)數(shù)據(jù)庫的Driver都是維護(hù)在一個(gè)CopyOnWriteArrayList中的,為了證明這一點(diǎn),貼兩段代碼,第一段在com.MySQL.jdbc.Driver下,也就是我們寫Class.forName("...")中的內(nèi)容:
public class Driver extends NonRegisteringDriver implements java.sql.Driver{ public Driver() throws SQLException { } static { try { DriverManager.registerDriver(new Driver()); } catch (SQLException E) { throw new RuntimeException("Can't register driver!"); } }}
看到com.mysql.jdbc.Driver調(diào)用了DriverManager的registerDriver方法,這個(gè)類在java.sql.DriverManager下:
public class DriverManager{ PRivate static final CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList(); private static volatile int loginTimeout = 0; private static volatile PrintWriter logWriter = null; private static volatile PrintStream logStream = null; private static final Object logSync = new Object(); static final SQLPermission SET_LOG_PERMISSION = new SQLPermission("setLog"); ...}
看到所有的DriverInfo都在CopyOnWriteArrayList中。既然看到了CopyOnWriteArrayList,我自然免不了要研究一番為什么JDK使用的是這個(gè)List。
首先提兩點(diǎn):
1、CopyOnWriteArrayList位于java.util.concurrent包下,可想而知,這個(gè)類是為并發(fā)而設(shè)計(jì)的
2、CopyOnWriteArrayList,顧名思義,Write的時(shí)候總是要Copy,也就是說對于CopyOnWriteArrayList,任何可變的操作(add、set、remove等等)都是伴隨復(fù)制這個(gè)動作的,后面會解讀CopyOnWriteArrayList的底層實(shí)現(xiàn)機(jī)制
四個(gè)關(guān)注點(diǎn)在CopyOnWriteArrayList上的答案
| 關(guān) 注 點(diǎn) | 結(jié) 論 |
| CopyOnWriteArrayList是否允許空 | 允許 |
| CopyOnWriteArrayList是否允許重復(fù)數(shù)據(jù) | 允許 |
| CopyOnWriteArrayList是否有序 | 有序 |
| CopyOnWriteArrayList是否線程安全 | 線程安全 |
如何向CopyOnWriteArrayList中添加元素
對于CopyOnWriteArrayList來說,增加、刪除、修改、插入的原理都是一樣的,所以用增加元素來分析一下CopyOnWriteArrayList的底層實(shí)現(xiàn)機(jī)制就可以了。先看一段代碼:
1 public static void main(String[] args)2 {3 List<Integer> list = new CopyOnWriteArrayList<Integer>();4 list.add(1);5 list.add(2);6 }
看一下這段代碼做了什么,先是第3行的實(shí)例化一個(gè)新的CopyOnWriteArrayList:
public class CopyOnWriteArrayList<E> implements List<E>, Randomaccess, Cloneable, java.io.Serializable { private static final long serialVersionUID = 8673264195747942595L; /** The lock protecting all mutators */ transient final ReentrantLock lock = new ReentrantLock(); /** The array, accessed only via getArray/setArray. */ private volatile transient Object[] array; ...}
public CopyOnWriteArrayList() { setArray(new Object[0]);}
final void setArray(Object[] a) { array = a;}
看到,對于CopyOnWriteArrayList來說,底層就是一個(gè)Object[] array,然后實(shí)例化一個(gè)CopyOnWriteArrayList,用圖來表示非常簡單:

就是這樣,Object array指向一個(gè)數(shù)組大小為0的數(shù)組。接著看一下,第4行的add一個(gè)整數(shù)1做了什么,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();}}
畫一張圖表示一下:

每一步都清楚地表示在圖上了,一次add大致經(jīng)歷了幾個(gè)步驟:
1、加鎖
2、拿到原數(shù)組,得到新數(shù)組的大小(原數(shù)組大小+1),實(shí)例化出一個(gè)新的數(shù)組來
3、把原數(shù)組的元素復(fù)制到新數(shù)組中去
4、新數(shù)組最后一個(gè)位置設(shè)置為待添加的元素(因?yàn)樾聰?shù)組的大小是按照原數(shù)組大小+1來的)
5、把Object array引用指向新數(shù)組
6、解鎖
整個(gè)過程看起來比較像ArrayList的擴(kuò)容。有了這個(gè)基礎(chǔ),我們再來看一下第5行的add了一個(gè)整數(shù)2做了什么,這應(yīng)該非常簡單了,還是畫一張圖來表示:

和前面差不多,就不解釋了。
另外,插入、刪除、修改操作也都是一樣,每一次的操作都是以對Object[] array進(jìn)行一次復(fù)制為基礎(chǔ)的,如果上面的流程看懂了,那么研究插入、刪除、修改的源代碼應(yīng)該不難。
普通List的缺陷
常用的List有ArrayList、LinkedList、Vector,其中前兩個(gè)是線程非安全的,最后一個(gè)是線程安全的。我有一種場景,兩個(gè)線程操作了同一個(gè)List,分別對同一個(gè)List進(jìn)行迭代和刪除,就如同下面的代碼:
public static class T1 extends Thread{ private List<Integer> list; public T1(List<Integer> list) { this.list = list; } public void run() { for (Integer i : list) { } }} public static class T2 extends Thread{ private List<Integer> list; public T2(List<Integer> list) { this.list = list; } public void run() { for (int i = 0; i < list.size(); i++) { list.remove(i); } }}
首先我在這兩個(gè)線程中放入ArrayList并啟動這兩個(gè)線程:
public static void main(String[] args){ List<Integer> list = new ArrayList<Integer>(); for (int i = 0; i < 10000; i++) { list.add(i); } T1 t1 = new T1(list); T2 t2 = new T2(list); t1.start(); t2.start();}
運(yùn)行結(jié)果為:
Exception in thread "Thread-0" java.util.ConcurrentModificationException at java.util.AbstractList$Itr.checkForComodification(AbstractList.java:372) at java.util.AbstractList$Itr.next(AbstractList.java:343) at com.xrq.test60.TestMain$T1.run(TestMain.java:19)
把ArrayList換成LinkedList,main函數(shù)的代碼就不貼了,運(yùn)行結(jié)果為:
Exception in thread "Thread-0" java.util.ConcurrentModificationException at java.util.LinkedList$ListItr.checkForComodification(LinkedList.java:761) at java.util.LinkedList$ListItr.next(LinkedList.java:696) at com.xrq.test60.TestMain$T1.run(TestMain.java:19)
可能有人覺得,這兩個(gè)線程都是線程非安全的類,所以不行。其實(shí)這個(gè)問題和線程安不安全沒有關(guān)系,換成Vector看一下運(yùn)行結(jié)果:
Exception in thread "Thread-0" java.util.ConcurrentModificationException at java.util.AbstractList$Itr.checkForComodification(AbstractList.java:372) at java.util.AbstractList$Itr.next(AbstractList.java:343) at com.xrq.test60.TestMain$T1.run(TestMain.java:19)
Vector雖然是線程安全的,但是只是一種相對的線程安全而不是絕對的線程安全,它只能夠保證增、刪、改、查的單個(gè)操作一定是原子的,不會被打斷,但是如果組合起來用,并不能保證線程安全性。比如就像上面的線程1在遍歷一個(gè)Vector中的元素、線程2在刪除一個(gè)Vector中的元素一樣,勢必產(chǎn)生并發(fā)修改異常,也就是fail-fast。
CopyOnWriteArrayList的作用
把上面的代碼修改一下,用CopyOnWriteArrayList:
public static void main(String[] args){ List<Integer> list = new CopyOnWriteArrayList<Integer>(); for (int i = 0; i < 10; i++) { list.add(i); } T1 t1 = new T1(list); T2 t2 = new T2(list); t1.start(); t2.start();}
可以運(yùn)行一下這段代碼,是沒有任何問題的。
看到我把元素?cái)?shù)量改小了一點(diǎn),因?yàn)槲覀儚纳厦娴姆治鲋袘?yīng)該可以看出,CopyOnWriteArrayList的缺點(diǎn),就是修改代價(jià)十分昂貴,每次修改都伴隨著一次的數(shù)組復(fù)制;但同時(shí)優(yōu)點(diǎn)也十分明顯,就是在并發(fā)下不會產(chǎn)生任何的線程安全問題,也就是絕對的線程安全,這也是為什么我們要使用CopyOnWriteArrayList的原因。
另外,有兩點(diǎn)必須講一下。我認(rèn)為CopyOnWriteArrayList這個(gè)并發(fā)組件,其實(shí)反映的是兩個(gè)十分重要的分布式理念:
(1)讀寫分離
我們讀取CopyOnWriteArrayList的時(shí)候讀取的是CopyOnWriteArrayList中的Object[] array,但是修改的時(shí)候,操作的是一個(gè)新的Object[] array,讀和寫操作的不是同一個(gè)對象,這就是讀寫分離。這種技術(shù)數(shù)據(jù)庫用的非常多,在高并發(fā)下為了緩解數(shù)據(jù)庫的壓力,即使做了緩存也要對數(shù)據(jù)庫做讀寫分離,讀的時(shí)候使用讀庫,寫的時(shí)候使用寫庫,然后讀庫、寫庫之間進(jìn)行一定的同步,這樣就避免同一個(gè)庫上讀、寫的IO操作太多
(2)最終一致
對CopyOnWriteArrayList來說,線程1讀取集合里面的數(shù)據(jù),未必是最新的數(shù)據(jù)。因?yàn)榫€程2、線程3、線程4四個(gè)線程都修改了CopyOnWriteArrayList里面的數(shù)據(jù),但是線程1拿到的還是最老的那個(gè)Object[] array,新添加進(jìn)去的數(shù)據(jù)并沒有,所以線程1讀取的內(nèi)容未必準(zhǔn)確。不過這些數(shù)據(jù)雖然對于線程1是不一致的,但是對于之后的線程一定是一致的,它們拿到的Object[] array一定是三個(gè)線程都操作完畢之后的Object array[],這就是最終一致。最終一致對于分布式系統(tǒng)也非常重要,它通過容忍一定時(shí)間的數(shù)據(jù)不一致,提升整個(gè)分布式系統(tǒng)的可用性與分區(qū)容錯(cuò)性。當(dāng)然,最終一致并不是任何場景都適用的,像火車站售票這種系統(tǒng)用戶對于數(shù)據(jù)的實(shí)時(shí)性要求非常非常高,就必須做成強(qiáng)一致性的。
最后總結(jié)一點(diǎn),隨著CopyOnWriteArrayList中元素的增加,CopyOnWriteArrayList的修改代價(jià)將越來越昂貴,因此,CopyOnWriteArrayList適用于讀操作遠(yuǎn)多于修改操作的并發(fā)場景中。
新聞熱點(diǎn)
疑難解答
圖片精選