Java 集合底層原理剖析(List、Set、Map、Queue)


Java 集合底層原理剖析(List、Set、Map、Queue)
溫馨提示:下面是以 Java 8 版本進行講解,除非有特定說明。

一、Java 集合介紹
Java 集合是一個存儲相同類型數據的容器,類似數組,集合可以不指定長度,但是數組必須指定長度。集合類主要從 Collection 和 Map 兩個根接口派生出來,比如常用的 ArrayList、LinkedList、HashMap、HashSet、ConcurrentHashMap 等等。

二、List

2.1 ArrayList
ArrayList 是基於動態數組實現,容量能自動增長的集合。隨機訪問效率高,隨機插入、隨機刪除效率低。線程不安全,多線程環境下可以使用 Collections.synchronizedList(list) 函數返回一個線程安全的 ArrayList 類,也可以使用 concurrent 並發包下的 CopyOnWriteArrayList 類。

2.1.1先說說synchronizedList(list) 底層源碼如下:

 static class SynchronizedList<E>
        extends SynchronizedCollection<E>
        implements List<E> {
        private static final long serialVersionUID = -7754090372962971524L;

        final List<E> list;

        SynchronizedList(List<E> list) {
            super(list);
            this.list = list;
        }
        SynchronizedList(List<E> list, Object mutex) {
            super(list, mutex);
            this.list = list;
        }

        public boolean equals(Object o) {
            if (this == o)
                return true;
            synchronized (mutex) {return list.equals(o);}
        }
        public int hashCode() {
            synchronized (mutex) {return list.hashCode();}
        }

        public E get(int index) {
            synchronized (mutex) {return list.get(index);}
        }
        public E set(int index, E element) {
            synchronized (mutex) {return list.set(index, element);}
        }
        public void add(int index, E element) {
            synchronized (mutex) {list.add(index, element);}
        }
        public E remove(int index) {
            synchronized (mutex) {return list.remove(index);}
        }

        public int indexOf(Object o) {
            synchronized (mutex) {return list.indexOf(o);}
        }
        public int lastIndexOf(Object o) {
            synchronized (mutex) {return list.lastIndexOf(o);}
        }

        public boolean addAll(int index, Collection<? extends E> c) {
            synchronized (mutex) {return list.addAll(index, c);}
        }

        public ListIterator<E> listIterator() {
            return list.listIterator(); // Must be manually synched by user
        }

        public ListIterator<E> listIterator(int index) {
            return list.listIterator(index); // Must be manually synched by user
        }

        public List<E> subList(int fromIndex, int toIndex) {
            synchronized (mutex) {
                return new SynchronizedList<>(list.subList(fromIndex, toIndex),
                                            mutex);
            }
        }

        @Override
        public void replaceAll(UnaryOperator<E> operator) {
            synchronized (mutex) {list.replaceAll(operator);}
        }
        @Override
        public void sort(Comparator<? super E> c) {
            synchronized (mutex) {list.sort(c);}
        }

        private Object readResolve() {
            return (list instanceof RandomAccess
                    ? new SynchronizedRandomAccessList<>(list)
                    : this);
        }
    }

使用方式如下,官方文檔就是下面的使用方式

List list = Collections.synchronizedList(new ArrayList());
      ...
  synchronized (list) {
      Iterator i = list.iterator(); // Must be in synchronized block
      while (i.hasNext())
          foo(i.next());
  }

既然封裝類內部已經加了對象鎖,為什么外部還要加一層對象鎖?

看源碼可知,Collections.synchronizedList中很多方法,比如equals,hasCode,get,set,add,remove,indexOf,lastIndexOf......

都添加了鎖,但是List中

Iterator<E> iterator();

這個方法沒有加鎖,不是線程安全的,所以如果要遍歷,還是必須要在外面加一層鎖。

使用Iterator迭代器的話,似乎也沒必要用Collections.synchronizedList的方法來包裝了——反正都是必須要使用Synchronized代碼塊包起來的。

所以總的來說,Collections.synchronizedList這種做法,適合不需要使用Iterator、對性能要求也不高的情況。SynchronizedList和Vector最主要的區別

  1. Vector擴容為原來的2倍長度,ArrayList擴容為原來1.5倍
  2. SynchronizedList有很好的擴展和兼容功能。他可以將所有的List的子類轉成線程安全的類。
  3. 使用SynchronizedList的時候,進行遍歷時要手動進行同步處理 。
  4. SynchronizedList可以指定鎖定的對象。

2.1.2再說說CopyOnWriteArrayList,同樣我們進源碼了解

CopyOnWriteArrayList是Java並發包中提供的一個並發容器,它是個線程安全且讀操作無鎖的ArrayList,寫操作則通過創建底層數組的新副本來實現,是一種讀寫分離的並發策略,我們也可以稱這種容器為"寫時復制器",Java並發包中類似的容器還有CopyOnWriteSet。

  我們都知道,集合框架中的ArrayList是非線程安全的,Vector雖是線程安全的,但由於簡單粗暴的鎖同步機制,性能較差。而CopyOnWriteArrayList則提供了另一種不同的並發處理策略(當然是針對特定的並發場景)。

  很多時候,我們的系統應對的都是讀多寫少的並發場景。CopyOnWriteArrayList容器允許並發讀,讀操作是無鎖的,性能較高。至於寫操作,比如向容器中添加一個元素,則首先將當前容器復制一份,然后在新副本上執行寫操作,結束之后再將原容器的引用指向新容器。

  優缺點分析

  了解了CopyOnWriteArrayList的實現原理,分析它的優缺點及使用場景就很容易了。

  優點:

  讀操作性能很高,因為無需任何同步措施,比較適用於讀多寫少的並發場景。Java的list在遍歷時,若中途有別的線程對list容器進行修改,則會拋出ConcurrentModificationException異常。而CopyOnWriteArrayList由於其"讀寫分離"的思想,遍歷和修改操作分別作用在不同的list容器,所以在使用迭代器進行遍歷時候,也就不會拋出ConcurrentModificationException異常了

  缺點:

  缺點也很明顯,一是內存占用問題,畢竟每次執行寫操作都要將原容器拷貝一份,數據量大時,對內存壓力較大,可能會引起頻繁GC二是無法保證實時性,Vector對於讀寫操作均加鎖同步,可以保證讀和寫的強一致性。而CopyOnWriteArrayList由於其實現策略的原因,寫和讀分別作用在新老不同容器上,在寫操作執行過程中,讀不會阻塞但讀取到的卻是老容器的數據。

源碼分析

  基本原理了解了,CopyOnWriteArrayList的代碼實現看起來就很容易理解了。

  添加操作:

public boolean add(E e) {
        //ReentrantLock加鎖,保證線程安全
        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();
        }
    }

添加的邏輯很簡單,先將原容器copy一份,然后在新副本上執行寫操作,之后再切換引用。當然此過程是要加鎖的。

  刪除操作

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)
                //如果要刪除的是列表末端數據,拷貝前len-1個數據到新副本上,再切換引用
                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的讀操作是不用加鎖的,性能很高

public E get(int index) {
        return get(getArray(), index);
    }

直接讀取即可,無需加鎖

動態數組,是指當數組容量不足以存放新的元素時,會創建新的數組,然后把原數組中的內容復制到新數組

主要屬性:

//存儲實際數據,使用transient修飾,序列化的時不會被保存
transient Object[] elementData;
//元素的數量,即容量。
private int size;

特征:

  1. 允許元素為 null;
  2. 查詢效率高、插入、刪除效率低,因為大量 copy 原來元素;
  3. 線程不安全。

使用場景:

  1. 需要快速隨機訪問元素
  2. 單線程環境

add(element) 流程:

  1. 判斷當前數組是否為空,如果是則創建長度為 10(默認)的數組,因為 new ArrayList 的時是沒有初始化
  2. 判斷是否需要擴容,如果當前數組的長度加 1(即 size+1)后是否大於當前數組長度,則進行擴容 grow();
  3. 最后在數組末尾添加元素,並 size+1。

grow() 流程:

  1. 創建新數組,長度擴大為原數組的 1.5 倍
  2. 如果擴大 1.5 倍還是不夠,則根據實際長度來擴容,比如 addAll() 場景;
  3. 將原數組的數據使用 System.arraycopy(native 方法)復制到新數組中。

add(index,element) 流程:

  1.檢查 index 是否在數組范圍內,假如數組長度是 2,則 index 必須 >=0 並且 <=2,否則報 IndexOutOfBoundsException 異常
  2.擴容檢查;
  3.通過拷貝方式,把數組位置為 index 至 size-1 的元素都往后移動一位,騰出位置之后放入元素,並 size+1。
set(index,element) 流程:

  1.檢查 index 是否在數組范圍內,假如數組長度是 2,則 index 必須 >=0 並且 <2;
  2.保留被覆蓋的值,因為最后需要返回舊的值;
  3.新元素覆蓋位置為 index 的舊元素,返回舊值。
get(index) 流程:

  1.判斷下標有沒有越界;
  2.通過數組下標來獲取元素,get 的時間復雜度是 O(1)。

remove(index) 流程:

  1.檢查指定位置是否在數組范圍內,假如數組長度是 2,則 index 必須 >=0 並且 < 2;
  2.保留要刪除的值,因為最后需要返回舊的值;
  3.計算出需要移動元素個數,再通過拷貝使數組內位置為 index+1 到 size-1 的元素往前移動一位,把數組最后一個元素設置為 null(精辟小技巧),返回舊值。
注意事項:

  1.new ArrayList 創建對象時,如果沒有指定集合容量則初始化為 0;如果有指定,則按照指定的大小初始化;
  2.擴容時,先將集合擴大 1.5 倍,如果還是不夠,則根據實際長度來擴容,保證都能存儲所有數據,比如 addAll() 場景。
  3.如果新擴容后數組長度大於(Integer.MAX_VALUE-8),則拋出 OutOfMemoryError

2.2 LinkedList

LinkedList 是可以在任何位置進行插入和移除操作的有序集合,它是基於雙向鏈表實現的,線程不安全。LinkedList 功能比較強大,可以實現棧、隊列或雙向隊列。

主要屬性:

//鏈表長度
transient int size = 0;
//頭部節點
transient Node<E> first;
//尾部節點
transient Node<E> last;

/\*\* \* 靜態內部類,存儲數據的節點 \*/
private static class Node\<E\> {
    //自身結點
    E item;
    //下一個節點
    Node<E> next;
    //上一個節點
    Node<E> prev;
}

特征:

  允許元素為 null;
  插入和刪除效率高,查詢效率低;
  順序訪問會非常高效,而隨機訪問效率(比如 get 方法)比較低;
  既能實現棧 Stack(后進先出),也能實現隊列(先進先出), 也能實現雙向隊列,因為提供了 xxxFirst()、xxxLast() 等方法;
  線程不安全。
使用場景:

  需要快速插入,刪除元素
  按照順序訪問其中的元素
  單線程環境
add() 流程:

  創建一個新結點,結點元素 item 為傳入參數,前繼節點 prev 為“當前鏈表 last 結點”,后繼節點 next 為 null;
  判斷當前鏈表 last 結點是否為空,如果是則把新建結點作為頭結點,如果不是則把新結點作為 last 結點。
  最后返回 true。
get(index,element) 流程:

  檢查 index 是否在數組范圍內,假如數組長度是 2,則 index 必須 >=0 並且 < 2;
  index 小於“雙向鏈表長度的 1/2”則從頭開始往后遍歷查找,否則從鏈表末尾開始向前遍歷查找。
remove() 流程:

  判斷 first 結點是否為空,如果是則報 NoSuchElementException 異常;
  如果不為空,則把待刪除結點的 next 結點的 prev 屬性賦值為 null,達到刪除頭結點的效果。
  返回刪除值。
2.3 Vector
Vector 是矢量隊列,也是基於動態數組實現,容量可以自動擴容。跟 ArrayList 實現原理一樣,但是 Vector 是線程安全,使用 Synchronized 實現線程安全,性能非常差,已被淘汰,使用 CopyOnWriteArrayList 替代 Vector。

主要屬性:

//存儲實際數據
protected Object[] elementData;
//動態數組的實際大小
protected int elementCount;
//動態數組的擴容系數
protected int capacityIncrement;

特征:

  允許元素為 null;
  查詢效率高、插入、刪除效率低,因為需要移動元素
  默認的初始化大小為 10,沒有指定增長系數則每次都是擴容一倍,如果擴容后還不夠,則直接根據參數長度來擴容
  線程安全,性能差(Synchronized),使用 CopyOnWriteArrayList 替代 Vector
**使用場景:**多線程環境

2.4 Stack
Stack 是棧,先進后出原則,Stack 繼承 Vector,也是通過數組實現,線程安全。因為效率比較低,不推薦使用,可以使用 LinkedList(線程不安全)或者 ConcurrentLinkedDeque(線程安全)來實現先進先出的效果。

**數據結構:**動態數組

**構造函數:**只有一個默認 Stack()

**特征:**先進后出

實現原理:

  Stack 執行 push() 時,將數據推進棧,即把數據追加到數組的末尾。
  Stack 執行 peek 時,取出棧頂數據,不刪除此數據,即獲取數組首個元素
  Stack 執行 pop 時,取出棧頂數據,在棧頂刪除數據,即刪除數組首個元素
  Stack 繼承於 Vector,所以 Vector 擁有的屬性和功能,Stack 都擁有,比如 add()、set() 等等。
2.5 CopyOnWriteArrayList
CopyOnWriteArrayList 是線程安全的 ArrayList,寫操作(add、set、remove 等等)時,把原數組拷貝一份出來,然后在新數組進行寫操作,操作完后,再將原數組引用指向到新數組。CopyOnWriteArrayList 可以替代 Collections.synchronizedList(List list)。

**數據結構:**動態數組

特征:

  線程安全;
  讀多寫少,比如緩存;
  不能保證實時一致性,只能保證最終一致性
缺點:

  寫操作,需要拷貝數組,比較消耗內存,如果原數組容量大的情況下,可能觸發頻繁的 Young GC 或者 Full GC
  不能用於實時讀的場景,因為讀取到數據可能是舊的,可以保證最終一致性
實現原理:

  CopyOnWriteArrayList 寫操作加了鎖,不然多線程進行寫操作時會復制多個副本;讀操作沒有加鎖,所以可以實現並發讀,但是可能讀到舊的數據,比如正在執行讀操作時,同時有多個寫操作在進行,遇到這種場景時,就會都到舊數據。

2.6 CopyOnWriteArraySet
CopyOnWriteArraySet 是線程安全的無序並且不能重復的集合,可以認為是線程安全的 HashSet,底層是通過 CopyOnWriteArrayList 機制實現。

**數據結構:**動態數組(CopyOnWriteArrayList),並不是散列表。

特征:

  線程安全
  讀多寫少,比如緩存
  不能存儲重復元素
2.7 ArrayList 和 Vector 區別
  Vector 線程安全,ArrayList 線程不安全;
  ArrayList 在擴容時默認是擴展 1.5 倍,Vector 是默認擴展 1 倍;
  ArrayList 支持序列化,Vector 不支持;
  Vector 提供 indexOf(obj, start) 接口,ArrayList 沒有;
  Vector 構造函數可以指定擴容增加系數,ArrayList 不可以
2.8 ArrayList 與 LinkedList 的區別
  ArrayList 的數據結構是動態數組,LinkedList 的數據結構是鏈表;
  ArrayList 不支持高效的插入和刪除元素,LinkedList 不支持高效的隨機訪問元素;
  ArrayList 的空間浪費在數組末尾預留一定的容量空間,LinkedList 的空間浪費在每一個結點都要消耗空間來存儲 prev、next 等信息。
三、Map
3.1 HashMap
  HashMap 是以key-value 鍵值對形式存儲數據,允許 key 為 null(多個則覆蓋),也允許 value 為 null。底層結構是數組 + 鏈表 + 紅黑樹

主要屬性:

  initialCapacity:初始容量,默認 16,2 的 N 次方
  loadFactor:負載因子,默認 0.75,用於擴容
  threshold:閾值,等於 initialCapacity * loadFactor,比如:16 * 0.75 = 12
  size:存放元素的個數,非 Node 數組長度。
Node

//存儲元素的數組
transient Node<K,V>[] table;
//存放元素的個數,非Node數組長度
transient int size;
//記錄結構性修改次數,用於快速失敗
transient int modCount;
//閾值
int threshold;
//負載因子,默認0.75,用於擴容
final float loadFactor;

 /\*\* \* 靜態內部類,存儲數據的節點 \*/
static class Node\<K,V\> implements Map.Entry\<K,V\> {
    //節點的hash值
    final int hash;
    //節點的key值
    final K key;
    //節點的value值
    V value;
    //下一個節點的引用
    Node<K,V> next;
}

**數據結構:**數組 + 單鏈表,Node 結構:hash|key|value|next

**只允許一個 key 為 Null(多個則覆蓋)但允許多個 value 為 Null **

  查詢、插入、刪除效率都高(集成了數組和單鏈表的特性)
** * 默認的初始化大小為 16,之后每次擴充為原來的 2 倍
  線程不安全
使用場景:

  快速增刪改查
  隨機存取
  緩存
  哈希沖突的解決方案:

    開放定址法
    再散列函數法
    鏈地址法(拉鏈法,常用)
put() 存儲的流程(Java 8):

  (1)計算待新增數據 key 的 hash 值;
  (2)判斷 Node[] 數組是否為空或者數據長度為 0 的情況,則需要進行初始化;
  (3)根據 hash 值通過位運算定計算出 Node 數組的下標,判斷該數組第一個 Node 節點是否有數據,如果沒有數據,則插入新值;
  (4)如果有數據,則根據具體情況進行操作,如下:
    1.如果該 Node 結點的 key(即鏈表頭結點)與待新增的 key 相等(== 或者 equals),則直接覆蓋值,最后返回舊值;
    2.如果該結構是樹形,則按照樹的方式插入新值;
    3.如果是鏈表結構,則判斷鏈表長度是否大於閾值 8,如果 >=8 並且數組長度 >=64 才轉為紅黑樹,如果 >=8 並且數組長度 < 64 則進行擴容;
    4.如果不需要轉為紅黑樹,則遍歷鏈表,如果找到 key 和 hash 值同時相等,則進行覆蓋返回舊值,如果沒有找到,則將新值插入到鏈表的最后面(尾插法);
    5.判斷數組長度是否大於閾值,如果是則進入擴容階段。
resize() 擴容的流程(Java 8):

  擴容過程比較復雜, 遷移算法與 Java 7 不一樣,Java 8 不需要每個元素都重新計算 hash,遷移過程中元素的位置要么是在原位置,要么是在原位置再移動 2 次冪的位置。

get() 查詢的流程(Java 8):

  根據 put() 方法的方式計算出數組的下標;
  遍歷數組下標對應的鏈表,如果找到 key 和 hash 值同時相等就返回對應的值,否則返回 null。
get() 注意事項:Java 8 沒有把 key 為 null 放到數組 table[0] 中。

remove() 刪除的流程(Java 8):

  根據 get() 方法的方式計算出數組的下標,即定位到存儲刪除元素的 Node 結點;
  如果待刪結點是頭節點,則用它的 next 結點頂替它作為頭節點;
  如果待刪結點是紅黑樹結點,則直接調用紅黑樹的刪除方法進行刪除;
  如果待刪結點是鏈表中的一個節點,則用待刪除結點的前一個節點的 next 屬性指向它的 next 結點;
  如果刪除成功則返回被刪結點的 value,否則返回 null。
remove() 注意事項:刪除單個 key,注意返回是的鍵值對中的 value。

為什么使用位運算(&)來代替取模運算(%):

  效率高,位運算直接對內存數據進行操作,不需轉成十進制,因此處理速度非常快;
  可以解決負數問題,比如:-17 % 10 = -7。
HashMap 在 Java 7 和 Java 8 中的區別:

  (1)存放數據的結點名稱不同,作用都一樣,存的都是 hashcode、key、value、next 等數據:
    Java 7:使用 Entry 存放數據
    Java 8:改名為 Node
  (2)定位數組下標位置方法不同:
    Java 7:計算 key 的 hash,將 hash 值進行了四次擾動,再進行取模得出;
    Java 8:計算 key 的 hash,將 hash 值進行高 16 位異或低 16 位,再進行與運算得出。
  (3)擴容算法不同:
    Java 7:擴容要重新計算 hash
    Java 8:不用重新計算
(4)put 方法插入鏈表位置不同:
    Java 7:頭插法
    Java 8:尾插法
(5)Java 8 引入了紅黑樹,當鏈表長度 >=8 時,並且同時數組的長度 >=64 時,鏈表就轉換為紅黑樹,利用紅黑樹快速增刪改查的特點提高 HashMap 的性能。
3.2 HashTable
和 HashMap 一樣,Hashtable 也是一個哈希散列表,Hashtable 繼承於 Dictionary,使用重入鎖 Synchronized 實現線程安全,key 和 value 都不允許為 Null。HashTable 已被高性能的 ConcurrentHashMap 代替。

主要屬性:

  initialCapacity:初始容量,默認 11。
  loadFactor:負載因子,默認 0.75。
  threshold:閾值。
  modCount:記錄結構性修改次數,用於快速失敗。

//真正存儲數據的數組
private transient Entry<?,?>[] table;
//存放元素的個數,非Entry數組長度
private transient int count;
//閾值
private int threshold;
//負載因子,默認0.75
private float loadFactor;
//記錄結構性修改次數,用於快速失敗
private transient int modCount = 0;

/\*\* \* 靜態內部類,存儲數據的節點 \*/
private static class Entry\<K,V\> implements Map.Entry\<K,V\> {
    //節點的hash值
    final int hash;
    //節點的key值
    final K key;
    //節點的value值
    V value;
    //下一個節點的引用
    Entry<K,V> next;
}

快速失敗原理是在並發場景下進行遍歷操作時,如果有另外一個線程對它執行了寫操作,此時迭代器可以發現並拋出 ConcurrentModificationException,而不需等到遍歷完后才報異常。

**數據結構:**鏈表的數組,數組 + 鏈表,Entry 結構:hash|key|value|next

特征:

  key 和 value 都不允許為 Null;
  HashTable 默認的初始大小為 11,之后每次擴充為原來的 2 倍;
線程安全。
原理:

與 HashMap 不一樣的流程是定位數組下標邏輯,HashTable 是在 key.hashcode() 后使用取模,HashMap 是位運算。HashTable 是 put() 之前進行判斷是否擴容 resize(),而 HashMap 是 put() 之后擴容。
更多參考(總結的不錯):(26條消息) Java 集合底層原理剖析(List、Set、Map、Queue)_快樂的工程師的博客-CSDN博客


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM