- ArrayList,LinkedList,Vector,CopyOnWriteArrayList 底層實現原理和四個集合的區別是什麼?
- 爲何工做中會經常使用ArrayList和CopyOnWriteArrayList?
- 若是面試官問你ArrayList和LinkedList有什麼區別?
上圖:node
ArrayList : 基於數組實現的非線程安全的集合。查詢元素快,插入,刪除中間元素慢。 LinkedList : 基於鏈表實現的非線程安全的集合。查詢元素慢,插入,刪除中間元素快。 Vector : 基於數組實現的線程安全的集合。線程同步(方法被synchronized修飾),性能比ArrayList差。 CopyOnWriteArrayList: 基於數組實現的線程安全的寫時複製集合。線程安全(ReentrantLock加鎖),性能比Vector高,適合讀多寫少的場景。面試
ArrayList : 查詢數據快,是由於數組能夠經過下標直接找到元素。 寫數據慢有兩個緣由:一是數組複製過程須要時間,二是擴容須要實例化新數組也須要時間。 LinkedList : 查詢數據慢,是由於鏈表須要遍歷每一個元素直到找到爲止。 寫數據快有一個緣由:除了實例化對象須要時間外,只須要修改指針便可完成添加和刪除元素。spring
注:這裏的快和慢是相對的。並非LinkedList的插入和刪除就必定比ArrayList快。明白其快慢的本質:ArrayList快在定位,慢在數組複製。LinkedList慢在定位,快在指針修改。數據庫
ArrayList 是基於動態數組實現的非線程安全的集合。當底層數組滿的狀況下還在繼續添加的元素時,ArrayList則會執行擴容機制擴大其數組長度。ArrayList查詢速度很是快,使得它在實際開發中被普遍使用。美中不足的是插入和刪除元素較慢,同時它並非線程安全的。數組
// 查詢元素 public E get(int index) { rangeCheck(index); // 檢查是否越界 return elementData(index); } // 順序添加元素 public boolean add(E e) { ensureCapacityInternal(size + 1); // 擴容機制 elementData[size++] = e; return true; } // 從數組中間添加元素 public void add(int index, E element) { rangeCheckForAdd(index); // 數組下標越界檢查 ensureCapacityInternal(size + 1); // 擴容機制 System.arraycopy(elementData, index, elementData, index + 1, size - index); // 複製數組 elementData[index] = element; // 替換元素 size++; } // 從數組中刪除元素 private void fastRemove(int index) { modCount++; int numMoved = size - index - 1; if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; // clear to let GC do its work }
從源碼中能夠得知,安全
ArrayList在執行查詢操做時: 第一步:先判斷下標是否越界。 第二步:而後在直接經過下標從數組中返回元素。性能優化
ArrayList在執行順序添加操做時: 第一步:經過擴容機制判斷原數組是否還有空間,若沒有則從新實例化一個空間更大的新數組,把舊數組的數據拷貝到新數組中。 第二步:在新數組的最後一位元素添加值。數據結構
ArrayList在執行中間插入操做時: 第一步:先判斷下標是否越界。 第二步:擴容。 第三步:若插入的下標爲i,則經過複製數組的方式將i後面的全部元素,日後移一位。 第四步:新數據替換下標爲i的舊元素。 刪除也是同樣:只是數組往前移了一位,最後一個元素設置爲null,等待JVM垃圾回收。架構
從上面的源碼分析,咱們能夠獲得一個結論和一個疑問。 結論是:ArrayList快在下標定位,慢在數組複製。 疑問是:可否將每次擴容的長度設置大點,減小擴容的次數,從而提升效率?其實每次擴容的長度大小是頗有講究的。若擴容的長度太大,會形成大量的閒置空間;若擴容的長度過小,會形成頻發的擴容(數組複製),效率更低。併發
LinkedList 是基於雙向鏈表實現的非線程安全的集合,它是一個鏈表結構,不能像數組同樣隨機訪問,必須是每一個元素依次遍歷直到找到元素爲止。其結構的特殊性致使它查詢數據慢。
// 查詢元素 public E get(int index) { checkElementIndex(index); // 檢查是否越界 return node(index).item; } Node<E> node(int index) { if (index < (size >> 1)) { // 相似二分法 Node<E> x = first; for (int i = 0; i < index; i++) x = x.next; return x; } else { Node<E> x = last; for (int i = size - 1; i > index; i--) x = x.prev; return x; } } // 插入元素 public void add(int index, E element) { checkPositionIndex(index); // 檢查是否越界 if (index == size) // 在鏈表末尾添加 linkLast(element); else // 在鏈表中間添加 linkBefore(element, node(index)); } void linkBefore(E e, Node<E> succ) { final Node<E> pred = succ.prev; final Node<E> newNode = new Node<>(pred, e, succ); succ.prev = newNode; if (pred == null) first = newNode; else pred.next = newNode; size++; modCount++; }
從源碼中能夠得知,
LinkedList在執行查詢操做時: 第一步:先判斷元素是靠近頭部,仍是靠近尾部。 第二步:若靠近頭部,則從頭部開始依次查詢判斷。和ArrayList的elementData(index)
相比固然是慢了不少。
LinkedList在插入元素的思路: 第一步:判斷插入元素的位置是鏈表的尾部,仍是中間。 第二步:若在鏈表尾部添加元素,直接將尾節點的下一個指針指向新增節點。 第三步:若在鏈表中間添加元素,先判斷插入的位置是否爲首節點,是則將首節點的上一個指針指向新增節點。不然先獲取當前節點的上一個節點(簡稱A),並將A節點的下一個指針指向新增節點,而後新增節點的下一個指針指向當前節點。
Vector 的數據結構和使用方法與ArrayList差很少。最大的不一樣就是Vector是線程安全的。從下面的源碼能夠看出,幾乎全部的對數據操做的方法都被synchronized關鍵字修飾。synchronized是線程同步的,當一個線程已經得到Vector對象的鎖時,其餘線程必須等待直到該鎖被釋放。從這裏就能夠得知Vector的性能要比ArrayList低。
若想要一個高性能,又是線程安全的ArrayList,可使用Collections.synchronizedList(list);
方法或者使用CopyOnWriteArrayList集合
public synchronized E get(int index) { if (index >= elementCount) throw new ArrayIndexOutOfBoundsException(index); return elementData(index); } public synchronized boolean add(E e) { modCount++; ensureCapacityHelper(elementCount + 1); elementData[elementCount++] = e; return true; } public synchronized boolean removeElement(Object obj) { modCount++; int i = indexOf(obj); if (i >= 0) { removeElementAt(i); return true; } return false; }
在這裏咱們先簡單瞭解一下CopyOnWrite容器。它是一個寫時複製的容器。當咱們往一個容器添加元素的時候,不是直接往當前容器添加,而是先將當前容器進行copy一份,複製出一個新的容器,而後對新容器裏面操做元素,最後將原容器的引用指向新的容器。因此CopyOnWrite容器是一種讀寫分離的思想,讀和寫不一樣的容器。
應用場景:適合高併發的讀操做(讀多寫少)。若寫的操做很是多,會頻繁複制容器,從而影響性能。
CopyOnWriteArrayList 寫時複製的集合,在執行寫操做(如:add,set,remove等)時,都會將原數組拷貝一份,而後在新數組上作修改操做。最後集合的引用指向新數組。
CopyOnWriteArrayList 和Vector都是線程安全的,不一樣的是:前者使用ReentrantLock類,後者使用synchronized關鍵字。ReentrantLock提供了更多的鎖投票機制,在鎖競爭的狀況下能表現更佳的性能。就是它讓JVM能更快的調度線程,纔有更多的時間去執行線程。這就是爲何CopyOnWriteArrayList的性能在大併發量的狀況下優於Vector的緣由。
private E get(Object[] a, int index) { return (E) a[index]; } 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(); } } private boolean remove(Object o, Object[] snapshot, int index) { final ReentrantLock lock = this.lock; lock.lock(); try { Object[] current = getArray(); int len = current.length; ...... Object[] newElements = new Object[len - 1]; System.arraycopy(current, 0, newElements, 0, index); System.arraycopy(current, index + 1, newElements, index, len - index - 1); setArray(newElements); return true; } finally { lock.unlock(); } }
更多Java源碼、數據庫、spring boot、內功心法等知識都是須要不斷學習、積累、實踐方能領會。所以我給你們推薦一個Java架構羣:895244712
,裏面有分佈式,微服務,性能優化等技術點底層原理的視頻,也有衆多想要提高的小夥伴討論技術,歡迎你們加羣一塊兒交流學習。
回到文章的標題,若是面試官問你ArrayList和LinkedList有什麼區別?
ArrayList和LinkedList都不是線程安全的,小併發量的狀況下可使用Vector,若併發量不少,且讀多寫少能夠考慮使用CopyOnWriteArrayList。 由於CopyOnWriteArrayList底層使用ReentrantLock鎖,比使用synchronized關鍵字的Vector能更好的處理鎖競爭的問題。
相信這個回答可以很好的幫你經過面試:p