圖解集合3:CopyOnWriteArrayList
初識CopyOnWriteArrayList
第一次見到CopyOnWriteArrayList,是在研究JDBC的時候,每一個數據庫的Driver都是維護在一個CopyOnWriteArrayList中的,為了證明這一點,貼兩段程式碼,第一段在com.mysql.jdbc.Driver下,也就是我們寫Class.forName("...")中的內容:
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呼叫了DriverManager的registerDriver方法,這個類在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使用的是這個List。
首先提兩點:
1、CopyOnWriteArrayList位於java.util.concurrent包下,可想而知,這個類是為併發而設計的
2、CopyOnWriteArrayList,顧名思義,Write的時候總是要Copy,也就是說對於CopyOnWriteArrayList,任何可變的操作(add、set、remove等等)都是伴隨複製這個動作的,後面會解讀CopyOnWriteArrayList的底層實現機制
四個關注點在CopyOnWriteArrayList上的答案
如何向CopyOnWriteArrayList中新增元素
對於CopyOnWriteArrayList來說,增加、刪除、修改、插入的原理都是一樣的,所以用增加元素來分析一下CopyOnWriteArrayList的底層實現機制就可以了。先看一段程式碼:
public static void main(String[] args)
{
List<Integer> list = new CopyOnWriteArrayList<Integer>();
list.add(1);
list.add(2);
}
看一下這段程式碼做了什麼,先是第3行的例項化一個新的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來說,底層就是一個Object[] array,然後例項化一個CopyOnWriteArrayList,用圖來表示非常簡單:
就是這樣,Object array指向一個數組大小為0的陣列。接著看一下,第4行的add一個整數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大致經歷了幾個步驟:
1、加鎖
2、拿到原陣列,得到新陣列的大小(原陣列大小+1),例項化出一個新的陣列來
3、把原陣列的元素複製到新陣列中去
4、新陣列最後一個位置設定為待新增的元素(因為新陣列的大小是按照原陣列大小+1來的)
5、把Object array引用指向新陣列
6、解鎖
整個過程看起來比較像ArrayList的擴容。有了這個基礎,我們再來看一下第5行的add了一個整數2做了什麼,這應該非常簡單了,還是畫一張圖來表示:
和前面差不多,就不解釋了。
另外,插入、刪除、修改操作也都是一樣,每一次的操作都是以對Object[] array進行一次複製為基礎的,如果上面的流程看懂了,那麼研究插入、刪除、修改的原始碼應該不難。
普通List的缺陷
常用的List有ArrayList、LinkedList、Vector,其中前兩個是執行緒非安全的,最後一個是執行緒安全的。我有一種場景,兩個執行緒操作了同一個List,分別對同一個List進行迭代和刪除,就如同下面的程式碼:
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);
}
}
}
首先我在這兩個執行緒中放入ArrayList並啟動這兩個執行緒:
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();
}
執行結果為:
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)
把ArrayList換成LinkedList,main函式的程式碼就不貼了,執行結果為:
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)
可能有人覺得,這兩個執行緒都是執行緒非安全的類,所以不行。其實這個問題和執行緒安不安全沒有關係,換成Vector看一下執行結果:
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雖然是執行緒安全的,但是隻是一種相對的執行緒安全而不是絕對的執行緒安全,它只能夠保證增、刪、改、查的單個操作一定是原子的,不會被打斷,但是如果組合起來用,並不能保證執行緒安全性。比如就像上面的執行緒1在遍歷一個Vector中的元素、執行緒2在刪除一個Vector中的元素一樣,勢必產生併發修改異常,也就是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();
}
可以執行一下這段程式碼,是沒有任何問題的。
看到我把元素數量改小了一點,因為我們從上面的分析中應該可以看出,CopyOnWriteArrayList的缺點,就是修改代價十分昂貴,每次修改都伴隨著一次的陣列複製;但同時優點也十分明顯,就是在併發下不會產生任何的執行緒安全問題,也就是絕對的執行緒安全,這也是為什麼我們要使用CopyOnWriteArrayList的原因。
另外,有兩點必須講一下。我認為CopyOnWriteArrayList這個併發元件,其實反映的是兩個十分重要的分散式理念:
(1)讀寫分離
我們讀取CopyOnWriteArrayList的時候讀取的是CopyOnWriteArrayList中的Object[] array,但是修改的時候,操作的是一個新的Object[] array,讀和寫操作的不是同一個物件,這就是讀寫分離。這種技術資料庫用的非常多,在高併發下為了緩解資料庫的壓力,即使做了快取也要對資料庫做讀寫分離,讀的時候使用讀庫,寫的時候使用寫庫,然後讀庫、寫庫之間進行一定的同步,這樣就避免同一個庫上讀、寫的IO操作太多
(2)最終一致
對CopyOnWriteArrayList來說,執行緒1讀取集合裡面的資料,未必是最新的資料。因為執行緒2、執行緒3、執行緒4四個執行緒都修改了CopyOnWriteArrayList裡面的資料,但是執行緒1拿到的還是最老的那個Object[] array,新新增進去的資料並沒有,所以執行緒1讀取的內容未必準確。不過這些資料雖然對於執行緒1是不一致的,但是對於之後的執行緒一定是一致的,它們拿到的Object[] array一定是三個執行緒都操作完畢之後的Object array[],這就是最終一致。最終一致對於分散式系統也非常重要,它通過容忍一定時間的資料不一致,提升整個分散式系統的可用性與分割槽容錯性。當然,最終一致並不是任何場景都適用的,像火車站售票這種系統使用者對於資料的實時性要求非常非常高,就必須做成強一致性的。
最後總結一點,隨著CopyOnWriteArrayList中元素的增加,CopyOnWriteArrayList的修改代價將越來越昂貴,因此,CopyOnWriteArrayList適用於讀操作遠多於修改操作的併發場景中。