1. 程式人生 > >Java 經典面試題:聊一聊 JUC 下的 CopyOnWriteArrayList

Java 經典面試題:聊一聊 JUC 下的 CopyOnWriteArrayList

ArrayList 是我們常用的工具類之一,但是在多執行緒的情況下,ArrayList 作為共享變數時,並不是執行緒安全的。主要有以下兩個原因: - 1、 ArrayList 自身的 elementData、size、modCount 在進行操作的時候,都沒有加鎖; - 2、這些變數沒有被 volatile 修飾,在多執行緒的情況下,對這些變數操作可能會出現值被覆蓋的情況; 如果我們想在多執行緒情況下使用 ArrayList 怎麼辦?有以下幾種辦法: - 使用 Collections.SynchronizedList ; - 使用 JUC 下的 CopyOnWriteArrayList; 先來看看 SynchronizedLis,Collections 其實就是對 ArrayList 進行了一個加鎖包裝,這個從原始碼中可以看出; ```java ...部分原始碼,完整原始碼請檢視 JDK 原始碼... public void add(int index, E element) { synchronized (mutex) {list.add(index, element);} } public E remove(int index) { synchronized (mutex) {return list.remove(index);} } ``` 對於 Collections.SynchronizedList 比較簡單,就是鎖包裝了一下,就不多說了~ CopyOnWriteArrayList 也是 JUC 下面的一個併發容器類。不知道你發現沒有,但凡你常用的集合類,在 JUC 下基本上都可以找到一個併發類,比如 hashMap 有對應的 ConcurrentHashMap。 CopyOnWriteArrayList 跟 ArrayList 在整體架構上並沒有什麼區別,底層都是基於陣列實現的。不同的地方大概有兩點: - 底層陣列被 volatile 關鍵字修飾; - 對陣列進行資料變更時加鎖; CopyOnWriteArrayList 的加鎖操作跟 Collections.SynchronizedList 簡單的加鎖還不一樣,CopyOnWriteArrayList 中的加鎖過程還是非常值得學習的。CopyOnWriteArrayList 的加鎖過程,大概可以概括為以下四步: - 1、加鎖; - 2、從原陣列中拷貝出新陣列; - 3、在新陣列上進行操作,並把新陣列賦值給陣列容器; - 4、解鎖; 結合原始碼來深入瞭解 CopyOnWriteArrayList 的併發實現,我們選擇 ArrayList 最簡單的將元素新增陣列尾部的操作來分析實現過程,原始碼如下: ```java /** * Appends the specified element to the end of this list. * * @param e element to be appended to this list * @return {@code true} (as specified by {@link Collection#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(); } } ``` CopyOnWriteArrayList 就是通過加鎖來說實現容器安全的,可能你會有疑問,**為什麼引入一個新陣列,陣列的拷貝還是消耗時間的,直接在原陣列上操作不就好了嗎?**。主要原因有以下兩點: - volatile 關鍵字修飾的是陣列,如果我們簡單的在原來陣列上修改其中某幾個元素的值,是無法觸發可見性的,我們必須通過修改陣列的記憶體地址才行,也就說要對陣列進行重新賦值才行。 - 在新的陣列上進行拷貝,對老陣列沒有任何影響,只有新陣列完全拷貝完成之後,外部才能訪問到,降低了在賦值過程中,老陣列資料變動的影響。比如經典的 `ConcurrentModificationException` 異常問題。 其他的新增方法就自己去檢視原始碼了,相差不多,基本上是一樣的。對陣列的刪除跟新增都是差不多,不同的地方是在刪除了時候,賦值給新陣列時會出現不同的選擇策略。我把原始碼貼上: ```java public E remove(int index) { final ReentrantLock lock = this.lock; // 加鎖 lock.lock(); try { Object[] elements = getArray(); int len = elements.length; E oldValue = get(elements, index); // 先計算出要移動的問題 int numMoved = len - index - 1; // 根據移動的位置選擇策略 if (numMoved == 0) setArray(Arrays.copyOf(elements, len - 1)); else { Object[] newElements = new Object[len - 1]; System.arraycopy(elements, 0, newElements, 0, index); System.arraycopy(elements, index + 1, newElements, index, numMoved); setArray(newElements); } return oldValue; } finally { //解鎖 lock.unlock(); } } ``` CopyOnWriteArrayList 還有其他的方法,在這裡我就不過多介紹了。根據你們自己的疑問去扒一扒 CopyOnWriteArrayList 的原始碼就知道了,總體來說 CopyOnWriteArrayList 並不難,甚至感覺比 ArrayList 要簡單。 總結一下:CopyOnWriteArrayList 是安全的併發容器,有以下兩個特點: - 1、對陣列的寫操作加鎖,讀操作不加鎖; - 2、通過加鎖 + 陣列拷貝+ volatile 來保證執行緒安全; > 歡迎關注公眾號【**網際網路平頭哥**】,一起成長,一起進步~。 ![網際網路平頭哥](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC80LzUvMTcxNDgwNGE1MDk2MjQ4NQ?x-oss-process=image/format,png#pic