1. 程式人生 > >java面試/筆試題目之Java常見集合(持續更新中)

java面試/筆試題目之Java常見集合(持續更新中)

宣告:題目大部分來源於Java後端公眾號,有些個人整理,但答案皆為個人整理,僅供參考。

目錄

Java中的集合

List 和 Set 區別

1.Set:集合中的物件不按特定方式排序(針對記憶體地址來說,即非線性),並且沒有重複物件。它的有些實現類能對集合中的物件按特定方式排序。

2.List:集合中的物件線性方式儲存,可以有重複物件,允許按照物件在集合中的索引位置檢索物件。有序可重複。

Set和hashCode以及equals方法的聯絡

List 和 Map 區別

1.Map:通過鍵值對進行取值,key-value一一對應的,其中key不可以重複,而value可以重複

區別:

Arraylist 與 LinkedList 區別

1.Arraylist(執行緒不安全):

2.LinkedList(執行緒不安全):

區別:

ArrayList 與 Vector 區別

1.Vector(執行緒安全):

區別:

HashMap 的工作原理及程式碼實現,什麼時候用到紅黑樹

1.HashMap(執行緒不安全,基於jdk1.7):

注意:

2.Hashtable(執行緒安全):

HashMap 和 Hashtable 的區別:

HashSet 和 HashMap 區別:

1.HashSet(執行緒不安全):

區別:

ConcurrentHashMap 的工作原理及程式碼實現,如何統計所有的元素個數

1.ConcurrentHashMap(執行緒安全):

總結

HashMap 和 ConcurrentHashMap 的區別

多執行緒情況下HashMap死迴圈的問題

介紹一下LinkedHashMap

HashMap出現Hash DOS攻擊的問題

手寫簡單的HashMap

看過那些Java集合類的原始碼

什麼是快速失敗的故障安全迭代器?

Iterator和ListIterator的區別

什麼是CopyOnWriteArrayList,它與ArrayList有何不同?

迭代器和列舉之間的區別

總結:


 

Java中的集合

Java中的集合主要分為value,key-value(Collection,Map)兩種,儲存值分為List和Set,儲存為key-value得失Map。

Collection介面中主要有這些方法:

boolean add(Object o)  :向集合中加入一個物件的引用   
void clear():刪除集合中所有的物件,即不再持有這些物件的引用  
boolean isEmpty()    :判斷集合是否為空   
boolean contains(Object o) : 判斷集合中是否持有特定物件的引用   
Iterartor iterator()  :返回一個Iterator物件,可以用來遍歷集合中的元素   
boolean remove(Object o) :從集合中刪除一個物件的引用   
int size()       :返回集合中元素的數目   
Object[] toArray()    : 返回一個數組,該陣列中包括集合中的所有元素
boolean equals(Object o):判斷值是否相等
int hashCode(): 返回當前集合的hash值,可以作為判斷地址是否想相等

Collection介面繼承 Iterable<T> 介面,這個介面可以返回一個迭代器,主要有一下三個方法:

List和Set都是繼承Collection介面。

List 和 Set 區別

1.Set:集合中的物件不按特定方式排序(針對記憶體地址來說,即非線性),並且沒有重複物件。它的有些實現類能對集合中的物件按特定方式排序。

  1. 不允許重複物件,只允許一個 null 元素,根據equals和hashcode判斷,一個物件要儲存在set中,必須重寫equals和hashcode方法;
  2. 無序容器,你無法保證每個元素的儲存順序,TreeSet通過 Comparator  或者 Comparable 維護了一個排序順序。
  3. Set 介面最流行的幾個實現類是 HashSet、LinkedHashSet 以及 TreeSet。最流行的是基於 HashMap 實現的 HashSet;TreeSet 還實現了 SortedSet 介面,因此 TreeSet 是一個根據其 compare() 和 compareTo() 的定義進行排序的有序容器。

2.List:集合中的物件線性方式儲存,可以有重複物件,允許按照物件在集合中的索引位置檢索物件。有序可重複。

  1. 可以允許重複的物件,可以插入多個null元素。

  2. 是一個有序容器,保持了每個元素的插入順序,輸出的順序就是插入的順序。

  3. 常用的實現類有 ArrayList、LinkedList 和 Vector。ArrayList 最為流行,它提供了使用索引的隨意訪問,而 LinkedList 則對於經常需要從 List 中新增或刪除元素的場合更為合適。

Set和hashCode以及equals方法的聯絡

因為set介面中是不允許存在重複的物件或者值的,所以需要對存入set中的物件或者值進行判斷,而hashCode和equals就是用來對這些物件和值進行判斷的。

List 和 Map 區別

1.Map:通過鍵值對進行取值,key-value一一對應的,其中key不可以重複,而value可以重複

區別:

  • Map用 put(k,v) / get(k),還可以使用containsKey()/containsValue()來檢查其中是否含有某個key/value。
  • List通過get()方法來一次取出一個元素。使用數字來選擇一堆物件中的一個,get(0)...。(add/get)
  • Collection沒有get()方法來取得某個元素。只能通過iterator()遍歷元素。

Arraylist 與 LinkedList 區別

1.Arraylist(執行緒不安全):

  • 底層是陣列(陣列在記憶體中是一塊連續的記憶體,如果插入或刪除元素需要移動記憶體),可以插入空資料
    public class ArrayList<E> extends AbstractList<E>
            implements List<E>, RandomAccess, Cloneable, java.io.Serializable
    

    實現了 RandomAccess 介面,所以支援隨機訪問

    private static final int DEFAULT_CAPACITY = 10;

    陣列的預設大小為 10。

  • 插入資料的時候,會先進行擴容校驗,新增元素時使用 ensureCapacityInternal() 方法來保證容量足夠,如果不夠時,需要使用 grow() 方法進行擴容,新容量的大小為 oldCapacity + (oldCapacity >> 1),也就是舊容量的 1.5 倍。

    public boolean add(E e) {
            ensureCapacityInternal(size + 1);  // Increments modCount!!
            elementData[size++] = e;
            return true;
        }

     

    • 首先進行擴容校驗。
    • 將插入的值放到尾部,並將 size + 1 。
  • 如果是呼叫 add(index,e) 在指定位置新增的話:
    public void add(int index, E element) {
            rangeCheckForAdd(index);
    
            ensureCapacityInternal(size + 1);  // Increments modCount!!
            //複製,向後移動
            System.arraycopy(elementData, index, elementData, index + 1,
                             size - index);
            elementData[index] = element;
            size++;
        }

     

    • 也是首先擴容校驗。
    • 接著對資料進行復制,目的是把 index 位置空出來放本次插入的資料,並將後面的資料向後移動一個位置。
      private void grow(int minCapacity) {
              // overflow-conscious code
              int oldCapacity = elementData.length;
              int newCapacity = oldCapacity + (oldCapacity >> 1);
              if (newCapacity - minCapacity < 0)
                  newCapacity = minCapacity;
              if (newCapacity - MAX_ARRAY_SIZE > 0)
                  newCapacity = hugeCapacity(minCapacity);
              // minCapacity is usually close to size, so this is a win:
              elementData = Arrays.copyOf(elementData, newCapacity);
          }

      擴容最終呼叫的程式碼,也是一個數組複製的過程。由此可見 ArrayList 的主要消耗是陣列擴容以及在指定位置新增資料,在日常使用時最好是指定大小,儘量減少擴容。更要減少在指定位置插入資料的操作。

  • 刪除元素

    public E remove(int index) {
        rangeCheck(index);
        modCount++;
        E oldValue = elementData(index);
        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
        return oldValue;
    }

    需要呼叫 System.arraycopy() 將 index+1 後面的元素都複製到 index 位置上,該操作的時間複雜度為 O(N),可以看出 ArrayList 刪除元素的代價是非常高的。

  • 由於 ArrayList 是基於動態陣列實現的,所以並不是所有的空間都被使用。因此使用了 transient 修飾,可以防止被自動序列化。

    transient Object[] elementData;

    儲存元素的陣列 elementData 使用 transient 修飾,該關鍵字宣告陣列預設不會被序列化。

     private void writeObject(java.io.ObjectOutputStream s)
            throws java.io.IOException{
            // Write out element count, and any hidden stuff
            int expectedModCount = modCount;
            s.defaultWriteObject();
    
            // Write out size as capacity for behavioural compatibility with clone()
            s.writeInt(size);
    
            // Write out all elements in the proper order.
            //只序列化了被使用的資料
            for (int i=0; i<size; i++) {
                s.writeObject(elementData[i]);
            }
    
            if (modCount != expectedModCount) {
                throw new ConcurrentModificationException();
            }
        }
    
        private void readObject(java.io.ObjectInputStream s)
            throws java.io.IOException, ClassNotFoundException {
            elementData = EMPTY_ELEMENTDATA;
    
            // Read in size, and any hidden stuff
            s.defaultReadObject();
    
            // Read in capacity
            s.readInt(); // ignored
    
            if (size > 0) {
                // be like clone(), allocate array based upon size not capacity
                ensureCapacityInternal(size);
    
                Object[] a = elementData;
                // Read in all elements in the proper order.
                for (int i=0; i<size; i++) {
                    a[i] = s.readObject();
                }
            }
        }

    當物件中自定義了 writeObject 和 readObject 方法時,JVM 會呼叫這兩個自定義方法來實現序列化與反序列化。序列化時需要使用 ObjectOutputStream 的 writeObject() 將物件轉換為位元組流並輸出。而 writeObject() 方法在傳入的物件存在 writeObject() 的時候會去反射呼叫該物件的 writeObject() 來實現序列化。反序列化使用的是 ObjectInputStream 的 readObject() 方法,原理類似。

2.LinkedList(執行緒不安全):

  • 底層是基於雙向連結串列實現的,(JDK1.7/8 之後取消了迴圈,修改為雙向連結串列),不要求記憶體是連續的,在當前元素存放下一個或上一個元素的地址。
  • 每次插入都是移動指標,改變引用指向即可,效率較高;
  • 查詢的時候使用二分法,利用了雙向連結串列的特性,如果index離連結串列頭比較近,就從節點頭部遍歷。否則就從節點尾部開始遍歷。使用空間(雙向連結串列)來換取時間。node()會以O(n/2)的效能去獲取一個結點;如果索引值大於連結串列大小的一半,那麼將從尾結點開始遍歷。
    public E get(int index) {
            checkElementIndex(index);
            return node(index).item;
        }
        
        Node<E> node(int index) {
            // assert isElementIndex(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;
            }
        }

    這樣的效率是非常低的,特別是當 index 越接近 size 的中間值時。

區別:

  • LinkedList 插入,刪除都是移動指標效率很高;查詢需要進行遍歷查詢,效率較低。
  • LinkedList比ArrayList更佔記憶體,因為LinkedList為每一個節點儲存了兩個引用,一個指向前一個元素,一個指向下一個元素。

  • ArrayList是可改變大小的陣列,而LinkedList是雙向連結串列

  • 在ArrayList的中間插入或刪除一個元素意味著這個列表中剩餘的元素都會被移動;而在LinkedList的中間插入或刪除一個元素的開銷是固定的

ArrayList 與 Vector 區別

1.Vector(執行緒安全):

  • 底層也是基於陣列實現的,但是add方法的時候使用了synchronized進行同步
    public synchronized boolean add(E e) {
            modCount++;
            ensureCapacityHelper(elementCount + 1);
            elementData[elementCount++] = e;
            return true;
        }
    public synchronized E get(int index) {
        if (index >= elementCount)
            throw new ArrayIndexOutOfBoundsException(index);
    
        return elementData(index);
    }
    這樣的話,開銷比較大,所以 Vector 是一個同步容器並不是一個併發容器。

區別:

  • ArrayList和Vector都採用線性連續儲存空間,當儲存空間不足的時候,Vector 每次擴容請求其大小的 2 倍空間,而 ArrayList 是 1.5 倍。
  • ArrayList執行緒不安全,Vector執行緒安全
  • Vector可以設定capacityIncrement,而ArrayList不可以,從字面理解就是capacity容量,Increment增加,容量增長的引數。

HashMap 的工作原理及程式碼實現,什麼時候用到紅黑樹

1.HashMap(執行緒不安全,基於jdk1.7):

  • hashmap是無序的,因為每次根據 key 的 hashcode 對映到 Entry 陣列上,所以遍歷出來的順序並不是寫入的順序
  • HashMap 底層是基於陣列和連結串列實現的,如圖所示,其中兩個重要的引數:容量和負載因子;容量的預設大小是 16,負載因子是 0.75,當 HashMap 的 size > 16*0.75 時就會發生擴容(容量和負載因子都可以自由調整)。 
  • 內部包含了一個 Entry 型別的陣列 table。

    transient Entry[] table;
    

    static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash;
    
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }
    
        public final K getKey() {
            return key;
        }
    
        public final V getValue() {
            return value;
        }
    
        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }
    
        public final boolean equals(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry e = (Map.Entry)o;
            Object k1 = getKey();
            Object k2 = e.getKey();
            if (k1 == k2 || (k1 != null && k1.equals(k2))) {
                Object v1 = getValue();
                Object v2 = e.getValue();
                if (v1 == v2 || (v1 != null && v1.equals(v2)))
                    return true;
            }
            return false;
        }
    
        public final int hashCode() {
            return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
        }
    
        public final String toString() {
            return getKey() + "=" + getValue();
        }
    }

    Entry 儲存著鍵值對。它包含了四個欄位,從 next 欄位我們可以看出 Entry 是一個連結串列。即陣列中的每個位置被當成一個桶,一個桶存放一個連結串列。HashMap 使用拉鍊法來解決衝突,同一個連結串列中存放雜湊值相同的 Entry

  • 拉鍊法的工作原理

    HashMap<String, String> map = new HashMap<>();
    map.put("K1", "V1");
    map.put("K2", "V2");
    map.put("K3", "V3");

    下面的桶對應陣列的一個元素,即陣列中的每個位置被當成一個桶,一個桶放一個連結串列。

    • 新建一個 HashMap,預設大小為 16;
    • 插入 <K1,V1> 鍵值對,先計算 K1 的 hashCode 為 115,使用除留餘數法得到所在的桶下標 115%16=3。
    • 插入 <K2,V2> 鍵值對,先計算 K2 的 hashCode 為 118,使用除留餘數法得到所在的桶下標 118%16=6。
    • 插入 <K3,V3> 鍵值對,先計算 K3 的 hashCode 為 118,使用除留餘數法得到所在的桶下標 118%16=6,插在 <K2,V2> 前面。
    • 應該注意到連結串列的插入是以頭插法方式進行的,例如上面的 <K3,V3> 不是插在 <K2,V2> 後面,而是插入在連結串列頭部。
    • 查詢需要分成兩步進行:

      • 計算鍵值對所在的桶;
      • 在連結串列上順序查詢,時間複雜度顯然和連結串列的長度成正比。
  • put方法:首先會將傳入的 Key 做hash運算計算出 hashcode,然後根據陣列長度取模計算出在陣列中的 index 下標。

    由於在計算中位運算比取模運算效率高的多,所以 HashMap 規定陣列的長度為 2^n 。這樣用 2^n - 1 做位運算與取模效果一致,並且效率還要高出許多。

    由於陣列的長度有限,所以難免會出現不同的 Key 通過運算得到的 index 相同,這種情況可以利用連結串列來解決,HashMap 會在 table[index]處形成連結串列,採用頭插法將資料插入到連結串列中。

    public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        // 鍵為 null 單獨處理
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);
        // 確定桶下標
        int i = indexFor(hash, table.length);
        // 先找出是否已經存在鍵為 key 的鍵值對,如果存在的話就更新這個鍵值對的值為 value
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
    
        modCount++;
        // 插入新鍵值對
        addEntry(hash, key, value, i);
        return null;
    }

    HashMap 允許插入鍵為 null 的鍵值對。但是因為無法呼叫 null 的 hashCode() 方法,也就無法確定該鍵值對的桶下標,只能通過強制指定一個桶下標來存放。HashMap 使用第 0 個桶存放鍵為 null 的鍵值對。

    private V putForNullKey(V value) {
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        addEntry(0, null, value, 0);
        return null;
    }

    使用連結串列的頭插法,也就是新的鍵值對插在連結串列的頭部,而不是連結串列的尾部。

    void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }
    
        createEntry(hash, key, value, bucketIndex);
    }
    
    void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        // 頭插法,連結串列頭部指向新的鍵值對
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }
    Entry(int h, K k, V v, Entry<K,V> n) {
        value = v;
        next = n;
        key = k;
        hash = h;
    }
    

    resize是擴容,預設擴為原來的2倍大小。

注意:

  • 在併發環境下使用 HashMap 容易出現死迴圈。

  • 併發場景發生擴容,呼叫 resize() 方法裡的 rehash() 時,容易出現環形連結串列。這樣當獲取一個不存在的 key 時,計算出的 index 正好是環形連結串列的下標時就會出現死迴圈。

  • 在 JDK1.8 中對 HashMap 進行了優化: 當 hash 碰撞之後寫入連結串列的長度超過了閾值(預設為8),連結串列將會轉換為紅黑樹。假設 hash 衝突非常嚴重,一個數組後面接了很長的連結串列,此時重新的時間複雜度就是 O(n) 。如果是紅黑樹,時間複雜度就是 O(logn) 。

2.Hashtable(執行緒安全):

  • 也是實現了Map介面,底層是連結串列和陣列;
  • 繼承了Dictionary<K,V>
  • Hashtable的synchronized是對整張hash表進行鎖定即讓執行緒獨享整張hash表,在安全同時造成了浪費。

HashMap 和 Hashtable 的區別:

  • HashMap執行緒不安全,Hashtable因為很多地方加了synchronized,所以它是執行緒安全的;
  • HashTable使用Enumeration,HashMap 使用Iterator。
  • HashMap不能保證元素的順序,HashMap能夠將鍵設為null,也可以將值設為null,但是隻有一個鍵為null,值可以多個為null,當get()方法返回null值時,可能是 HashMap中沒有該鍵,也可能使該鍵所對應的值為null。因此,在HashMap中不能由get()方法來判斷HashMap中是否存在某個鍵, 而應該用containsKey()方法來判斷。Hashtable不能將鍵和值設為null,否則執行時會報空指標異常錯誤;
  • hash值的使用方式不同,Hashtable直接使用物件的hashCode,對table陣列的長度直接進行取模;而HashMap計算hash對key的hashcode進行了二次hash,以獲得更好的雜湊值,然後對table陣列長度取模;
  • Hashtable
    
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    
    HashMap
    
    static int hash(int h) {
         h ^= (h >>> 20) ^ (h >>> 12);
         return h ^ (h >>> 7) ^ (h >>> 4);
     }
    
    static int indexFor(int h, int length) {
         return h & (length-1);
     }
  • HashMap沒有contains方法,而Hashtabl有contains方法。
    //以下是Hashtable的方法
    public synchronized boolean contains(Object value)
    public synchronized boolean containsKey(Object key)
    public boolean containsValue(Object value)
    
    //以下是HashMap中的方法,注意,沒有contains方法
    public boolean containsKey(Object key)
    public boolean containsValue(Object value)
    

     

  • Hashtable中hash預設陣列大小是11,增加的方式是old*2+1;HashMap中hash陣列大小預設是16,而且一定是2的倍數,HashMap會將其擴充為2的冪次方大小
  • Hashtable、HashMap都使用了 Iterator。而由於歷史原因,Hashtable還使用了Enumeration的方式 。
  • 兩者儲存規則不一樣:
    • HashMap的儲存規則:優先使用陣列儲存, 如果出現Hash衝突, 將在陣列的該位置拉伸出連結串列進行儲存(在連結串列的尾部進行新增), 如果連結串列的長度大於設定值後, 將連結串列轉為紅黑樹.

    • HashTable的儲存規則:優先使用陣列儲存, 儲存元素時, 先取出下標上的元素(可能為null), 然後新增到陣列元素Entry物件的next屬性中(在連結串列的頭部進行新增).出現Hash衝突時, 新元素next屬性會指向衝突的元素. 如果沒有Hash衝突, 則新元素的next屬性就是null。

      Entry<K,V> e = (Entry<K,V>) tab[index];
      tab[index] = new Entry<>(hash, key, value, e);

       

參照:https://blog.csdn.net/wangxing233/article/details/79452946

HashSet 和 HashMap 區別:

1.HashSet(執行緒不安全):

  • 不允許儲存重複元素的集合
  • 基於雜湊表實現,支援快速查詢,但不支援有序性操作。
  • 使用 Iterator 遍歷 HashSet 得到的結果是不確定的。
  • 成員變數:
    private transient HashMap<E,Object> map;
    
        // Dummy value to associate with an Object in the backing Map
        private static final Object PRESENT = new Object();

    兩個變數:

    • map :用於存放最終資料的。
    • PRESENT :是所有寫入 map 的 value 值。
  • 建構函式:利用了 HashMap 初始化了 map 

    public HashSet() {
            map = new HashMap<>();
        }
        
        public HashSet(int initialCapacity, float loadFactor) {
            map = new HashMap<>(initialCapacity, loadFactor);
        }    

     

 

  •  add方法:

    public boolean add(E e) {
            return map.put(e, PRESENT)==null;
        }

    Hashtable將存放的物件當做了 HashMap 的健,value 都是相同的 PRESENT 。由於 HashMap 的 key 是不能重複的,所以每當有重複的值寫入到 HashSet 時,value 會被覆蓋,但 key 不會受到影響,這樣就保證了 HashSet 中只能存放不重複的元素。

HashSet 的原理比較簡單,幾乎全部藉助於 HashMap 來實現的。所以 HashMap 會出現的問題 HashSet 依然不能避免。

區別:

  • HashMap實現了Map介面,而Hashtable實現Set介面;

  • HashMap儲存鍵值對,Hashtable僅儲存物件;
  • HashMap呼叫put()向map中新增元素;Hashtable呼叫add()方法向Set中新增元素;
  • HashMap比較快,因為是使用唯一的鍵來獲取物件

ConcurrentHashMap 的工作原理及程式碼實現,如何統計所有的元素個數

1.ConcurrentHashMap(執行緒安全):

  • 儲存結構:
    static final class HashEntry<K,V> {
        final int hash;
        final K key;
        volatile V value;
        volatile HashEntry<K,V> next;
    }
    ConcurrentHashMap 和 HashMap 實現上類似,最主要的差別是 ConcurrentHashMap 採用了分段鎖(Segment),每個分段鎖維護著幾個桶(HashEntry),多個執行緒可以同時訪問不同分段鎖上的桶,從而使其併發度更高(併發度就是 Segment 的個數)。
  • 資料結構(JDK1.7):

如圖所示,是由 Segment 陣列、HashEntry 陣列組成,和 HashMap 一樣,仍然是陣列加連結串列組成ConcurrentHashMap 採用了分段鎖技術,其中 Segment 繼承於 ReentrantLock。不會像 HashTable 那樣不管是 put 還是 get 操作都需要做同步處理,理論上 ConcurrentHashMap 支援 CurrencyLevel (Segment 陣列數量)的執行緒併發。每當一個執行緒佔用鎖訪問一個 Segment 時,不會影響到其他的 Segment

  • 預設的併發級別為 16,也就是說預設建立 16 個 Segment。
  • get 方法:ConcurrentHashMap 的 get 方法是非常高效的,因為整個過程都不需要加鎖。

    只需要將 Key 通過 Hash 之後定位到具體的 Segment ,再通過一次 Hash 定位到具體的元素上。由於 HashEntry 中的 value 屬性是用 volatile 關鍵詞修飾的,保證了記憶體可見性,所以每次獲取時都是最新值。

  • put 方法:

    static final class HashEntry<K,V> {
            final int hash;
            final K key;
            volatile V value;
            volatile HashEntry<K,V> next;
    
            HashEntry(int hash, K key, V value, HashEntry<K,V> next) {
                this.hash = hash;
                this.key = key;
                this.value = value;
                this.next = next;
            }
        }

     

雖然 HashEntry 中的 value 是用 volatile 關鍵詞修飾的,但是並不能保證併發的原子性,所以 put 操作時仍然需要加鎖處理。首先也是通過 Key 的 Hash 定位到具體的 Segment,在 put 之前會進行一次擴容校驗。這裡比 HashMap 要好的一點是:HashMap 是插入元素之後再看是否需要擴容,有可能擴容之後後續就沒有插入就浪費了本次擴容(擴容非常消耗效能)。而 ConcurrentHashMap 不一樣,它是在將資料插入之前檢查是否需要擴容,之後再做插入操作。

  • size 方法:每個 Segment 維護了一個 count 變數來統計該 Segment 中的鍵值對個數。
    /**
     * The number of elements. Accessed only either within locks
     * or among other volatile reads that maintain visibility.
     */
    transient int count;

    在執行 size 操作時,需要遍歷所有 Segment 然後把 count 累計起來。ConcurrentHashMap 在執行 size 操作時先嚐試不加鎖,如果連續兩次不加鎖操作得到的結果一致,那麼可以認為這個結果是正確的。嘗試次數使用 RETRIES_BEFORE_LOCK 定義,該值為 2,retries 初始值為 -1,因此嘗試次數為 3。如果嘗試的次數超過 3 次,就需要對每個 Segment 加鎖。

    /**
     * Number of unsynchronized retries in size and containsValue
     * methods before resorting to locking. This is used to avoid
     * unbounded retries if tables undergo continuous modification
     * which would make it impossible to obtain an accurate result.
     */
    static final int RETRIES_BEFORE_LOCK = 2;
    
    public int size() {
        // Try a few times to get accurate count. On failure due to
        // continuous async changes in table, resort to locking.
        final Segment<K,V>[] segments = this.segments;
        int size;
        boolean overflow; // true if size overflows 32 bits
        long sum;         // sum of modCounts
        long last = 0L;   // previous sum
        int retries = -1; // first iteration isn't retry
        try {
            for (;;) {
                // 超過嘗試次數,則對每個 Segment 加鎖
                if (retries++ == RETRIES_BEFORE_LOCK) {
                    for (int j = 0; j < segments.length; ++j)
                        ensureSegment(j).lock(); // force creation
                }
                sum = 0L;
                size = 0;
                overflow = false;
                for (int j = 0; j < segments.length; ++j) {
                    Segment<K,V> seg = segmentAt(segments, j);
                    if (seg != null) {
                        sum += seg.modCount;
                        int c = seg.count;
                        if (c < 0 || (size += c) < 0)
                            overflow = true;
                    }
                }
                // 連續兩次得到的結果一致,則認為這個結果是正確的
                if (sum == last)
                    break;
                last = sum;
            }
        } finally {
            if (retries > RETRIES_BEFORE_LOCK) {
                for (int j = 0; j < segments.length; ++j)
                    segmentAt(segments, j).unlock();
            }
        }
        return overflow ? Integer.MAX_VALUE : size;
    }

    每個 Segment 都有一個 modCount 變數,每當進行一次 put remove 等操作,modCount 將會 +1。只要 modCount 發生了變化就認為容器的大小也在發生變化。

  • JDK1.8的實現:

拋棄了原有的 Segment 分段鎖,而採用了 CAS + synchronized 來保證併發安全性。

也將 1.7 中存放資料的 HashEntry 改為 Node,但作用都是相同的。其中的 val next 都用了 volatile 修飾,保證了可見性。

put方法:

  • 根據 key 計算出 hashcode 。
  • 判斷是否需要進行初始化。
  • f 即為當前 key 定位出的 Node,如果為空表示當前位置可以寫入資料,利用 CAS 嘗試寫入,失敗則自旋保證成功。
  • 如果當前位置的 hashcode == MOVED == -1,則需要進行擴容。
  • 如果都不滿足,則利用 synchronized 鎖寫入資料。
  • 如果數量大於 TREEIFY_THRESHOLD 則要轉換為紅黑樹。

get方法:

  • 根據計算出來的 hashcode 定址,如果就在桶上那麼直接返回值。
  • 如果是紅黑樹那就按照樹的方式獲取值。
  • 都不滿足那就按照連結串列的方式遍歷獲取值。

總結

1.8 在 1.7 的資料結構上做了大的改動,採用紅黑樹之後可以保證查詢效率(O(logn)),甚至取消了 ReentrantLock 改為了 synchronized,這樣可以看出在新版的 JDK 中對 synchronized 優化是很到位的。

HashMap 和 ConcurrentHashMap 的區別

  • HashMap執行緒不安全,而ConcurrentHashMap執行緒安全;

多執行緒情況下HashMap死迴圈的問題

  • 容量大於  總量*負載因子  發生擴容時會出現環形連結串列從而導致死迴圈。
  • 併發場景發生擴容,呼叫 resize() 方法裡的 rehash() 時,容易出現環形連結串列。這樣當獲取一個不存在的 key 時,計算出的 index 正好是環形連結串列的下標時就會出現死迴圈。

https://www.cnblogs.com/dongguacai/p/5599100.html

https://blog.csdn.net/linsongbin1/article/details/54708694

介紹一下LinkedHashMap

  • 這是一個有序的,底層是繼承於 HashMap 實現的,由一個雙向連結串列所構成,具有和 HashMap 一樣的快速查詢特性。

  • LinkedHashMap 的排序方式有兩種:

    • 根據寫入順序排序。
    • 根據訪問順序排序,每次 get 都會將訪問的值移動到連結串列末尾,這樣重複操作就能得到一個按照訪問順序排序的連結串列。
  • 資料結構,通過以下程式碼除錯可以看到 map 的組成:
    @Test
    	public void test(){
    		Map<String, Integer> map = new LinkedHashMap<String, Integer>();
    		map.put("1",1) ;
    		map.put("2",2) ;
    		map.put("3",3) ;
    		map.put("4",4) ;
    		map.put("5",5) ;
    		System.out.println(map.toString());
    	}

 /**
     * The head of the doubly linked list.
     */
    private transient Entry<K,V> header;

    /**
     * The iteration ordering method for this linked hash map: <tt>true</tt>
     * for access-order, <tt>false</tt> for insertion-order.
     *
     * @serial
     */
    private final boolean accessOrder;
    
    private static class Entry<K,V> extends HashMap.Entry<K,V> {
        // These fields comprise the doubly linked list used for iteration.
        Entry<K,V> before, after;

        Entry(int hash, K key, V value, HashMap.Entry<K,V> next) {
            super(hash, key, value, next);
        }
    }  

其中 Entry 繼承於 HashMap 的 Entry,並新增了上下節點的指標,也就形成了雙向連結串列。還有一個 header 的成員變數,是這個雙向連結串列的頭結點。

第一個類似於 HashMap 的結構,利用 Entry 中的 next 指標進行關聯。下邊則是 LinkedHashMap 如何達到有序的關鍵。就是利用了頭節點和其餘的各個節點之間通過 Entry 中的 after 和 before 指標進行關聯。其中還有一個 accessOrder 成員變數,預設是 false,預設按照插入順序排序,為 true 時按照訪問順序排序,也可以呼叫:

這個構造方法可以顯示的傳入 accessOrder

public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }
  • 構造方法就是呼叫HashMap 的構造方法:

    public LinkedHashMap() {
            super();
            accessOrder = false;
        }
    // HashMap實現:
     public HashMap(int initialCapacity, float loadFactor) {
            if (initialCapacity < 0)
                throw new IllegalArgumentException("Illegal initial capacity: " +
                                                   initialCapacity);
            if (initialCapacity > MAXIMUM_CAPACITY)
                initialCapacity = MAXIMUM_CAPACITY;
            if (loadFactor <= 0 || Float.isNaN(loadFactor))
                throw new IllegalArgumentException("Illegal load factor: " +
                                                   loadFactor);
    
            this.loadFactor = loadFactor;
            threshold = initialCapacity;
            //HashMap 只是定義了改方法,具體實現交給了 LinkedHashMap
            init();
        }
    

    可以看到裡面有一個空的 init(),具體是由 LinkedHashMap 來實現的:

     @Override
        void init() {
            header = new Entry<>(-1, null, null, null);
            header.before = header.after = header;
        }

    其實也就是對 header 進行了初始化,從這個方法可以看出,實現了雙向。

  • put() 方法:主體的實現都是藉助於 HashMap 來完成的,只是對其中的 recordAccess(), addEntry(), createEntry() 進行了重寫。如下是HashMap的put方法:

    public V put(K key, V value) {
            if (table == EMPTY_TABLE) {
                inflateTable(threshold);
            }
            if (key == null)
                return putForNullKey(value);
            int hash = hash(key);
            int i = indexFor(hash, table.length);
            for (Entry<K,V> e = table[i]; e != null; e = e.next) {
                Object k;
                if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                    V oldValue = e.value;
                    e.value = value;
                    //空實現,交給 LinkedHashMap 自己實現
                    e.recordAccess(this);
                    return oldValue;
                }
            }
    
            modCount++;
            // LinkedHashMap 對其重寫
            addEntry(hash, key, value, i);
            return null;
        }
        
        // LinkedHashMap 對其重寫
        void addEntry(int hash, K key, V value, int bucketIndex) {
            if ((size >= threshold) && (null != table[bucketIndex])) {
                resize(2 * table.length);
                hash = (null != key) ? hash(key) : 0;
                bucketIndex = indexFor(hash, table.length);
            }
    
            createEntry(hash, key, value, bucketIndex);
        }
        
        // LinkedHashMap 對其重寫
        void createEntry(int hash, K key, V value, int bucketIndex) {
            Entry<K,V> e = table[bucketIndex];
            table[bucketIndex] = new Entry<>(hash, key, value, e);
            size++;
        }       
    //就是判斷是否是根據訪問順序排序,如果是則需要將當前這個 Entry 移動到連結串列的末尾
            void recordAccess(HashMap<K,V> m) {
                LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
                if (lm.accessOrder) {
                    lm.modCount++;
                    remove();
                    addBefore(lm.header);
                }
            }
            
            
        //呼叫了 HashMap 的實現,並判斷是否需要刪除最少使用的 Entry(預設不刪除)    
        void addEntry(int hash, K key, V value, int bucketIndex) {
            super.addEntry(hash, key, value, bucketIndex);
    
            // Remove eldest entry if instructed
            Entry<K,V> eldest = header.after;
            if (removeEldestEntry(eldest)) {
                removeEntryForKey(eldest.key);
            }
        }
        
        void createEntry(int hash, K key, V value, int bucketIndex) {
            HashMap.Entry<K,V> old = table[bucketIndex];
            Entry<K,V> e = new Entry<>(hash, key, value, old);
            //就多了這一步,將新增的 Entry 加入到 header 雙向連結串列中
            table[bucketIndex] = e;
            e.addBefore(header);
            size++;
        }
        
            //寫入到雙向連結串列中
            private void addBefore(Entry<K,V> existingEntry) {
                after  = existingEntry;
                before = existingEntry.before;
                before.after = this;
                after.before = this;
            }  

    以上是LinkedHashMap 的實現。

  • get 方法,LinkedHashMap 的 get() 方法也重寫了:

    public V get(Object key) {
            Entry<K,V> e = (Entry<K,V>)getEntry(key);
            if (e == null)
                return null;
                
            //多了一個判斷是否是按照訪問順序排序,是則將當前的 Entry 移動到連結串列頭部。   
            e.recordAccess(this);
            return e.value;
        }
        
        void recordAccess(HashMap<K,V> m) {
            LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
            if (lm.accessOrder) {
                lm.modCount++;
                
                //刪除
                remove();
                //新增到頭部
                addBefore(lm.header);
            }
        }

    clear()方法:

    //只需要把指標都指向自己即可,原本那些 Entry 沒有引用之後就會被 JVM 自動回收。
        public void clear() {
            super.clear();
            header.before = header.after = header;
        }

    總的來說 LinkedHashMap 其實就是對 HashMap 進行了拓展,使用了雙向連結串列來保證了順序性。因為是繼承與 HashMap 的,所以一些 HashMap 存在的問題 LinkedHashMap 也會存在,比如不支援併發等。

HashMap出現Hash DOS攻擊的問題

手寫簡單的HashMap

看過那些Java集合類的原始碼

什麼是快速失敗的故障安全迭代器?

快速失敗的Java迭代器可能會引發ConcurrentModifcationException在底層集合迭代過程中被修改。故障安全作為發生在例項中的一個副本迭代是不會丟擲任何異常的。快速失敗的故障安全範例定義了當遭遇故障時系統是如何反應的。例如,用於失敗的快速迭代器ArrayList和用於故障安全的迭代器ConcurrentHashMap。

Iterator和ListIterator的區別

●ListIterator有add()方法,可以向List中新增物件,而Iterator不能。

●ListIterator和Iterator都有hasNext()和next()方法,可以實現順序向後遍歷,但是ListIterator有hasPrevious()和previous()方法,可以實現逆向(順序向前)遍歷。Iterator就不可以。

●ListIterator可以定位當前的索引位置,nextIndex()和previousIndex()可以實現。Iterator沒有此功能。

●都可實現