Java集合類之HashMap原理小結

1. 認識HashMap

HashMap是用來存儲key-value鍵值對的數據結構。
當咱們建立HashMap的時候,若是不指定任何參數,它會爲咱們建立一個初始容量爲16,負載因子爲0.75的HashMap (load factor,記錄數/數組長度)。當loadFactor達到0.75或指定值的時候,HashMap的總容量自動擴展一倍。html

它的底層採用Entry數組來保存全部的key-value對。當須要存儲一個Entry對象時,會根據Hash算法(key的hashCode值)來決定其存儲位置;當須要取出一個Entry時,也會根據Hash算法找到其存儲位置,直接取出該Entry。因而可知:HashMap之因此能快速存、取它所包含的Entry,徹底相似於現實生活中母親從小教咱們的:不一樣的東西要放在不一樣的位置,須要時才能快速找到它。java

若是兩個Entry的key的hashCode()返回值相同,那它們的存儲位置相同。若是這兩個Entry的key經過equals()比較返回true,新添加Entry的value將覆蓋集合中原有Entry的value,但key不會覆蓋。若是這兩個Entry的key經過equals()比較返回false,新添加的Entry將與集合中原有Entry造成Entry鏈,並且新添加的Entry位於Entry鏈的頭部。咱們來看下圖:node

hashmap%E5%8E%9F%E7%90%86.png
注:圖片源自http://www.admin10000.com/doc...算法

2. 小結

HashMap底層實現:數組+鏈表+紅黑樹
一般,只使用Entry數組存放鍵值對,key的hashcode()值決定它的存放位置,equals()方法決定最終的值。
若是hash算法設計的足夠好,是不會發生碰撞衝突的,但實際中確定不存在這麼完美的事情。
當key的hashcode()相同,equals()方法返回不一樣時,會在相同的位置上造成一個鏈表,當鏈表長度大於8的時候,會轉化成紅黑樹,鏈表的查找的時間複雜度爲O(n),而紅黑樹爲O(lgn),會提升查詢的性能。
當Entry數組不足以容納更多的元素的時候,以負載因子爲0.75,數組長度爲20來講,當數組元素數到達15的時候,會自動觸發一次resize操做,會把舊的數據映射到新的哈希表,數組擴容到原來的2倍。編程

resize在多線程環境下,可能產生條件競爭
由於若是兩個線程都發現HashMap須要從新調整大小了,它們會同時試着調整大小。
在調整大小的過程當中,存儲在鏈表中的元素的次序會反過來,由於移動到新的bucket位置的時候,HashMap並不會將元素放在鏈表的尾部,而是放在頭部,這是爲了不尾部遍歷(tail traversing,不然針對key的hashcode相同的Entry每次添加還要定位到尾節點)。
若是條件競爭發生了,可能出現環形鏈表。以後當咱們get(key)操做時,就有可能發生死循環。
另外,既然都有併發的問題了,咱們就該使用ConcurrentHashMap了。數組

不使用HashTable的緣由
它使用synchronized來保證線程安全,會鎖住整個哈希表。在線程競爭激烈的狀況下效率很是低下,當一個線程訪問HashTable的同步方法時,其它線程訪問HashTable的同步方法只能進入阻塞或輪詢狀態。緩存

3. ConcurrentHashMap

核心:採用segment分段鎖來保護不一樣段的數據,是線程安全且高效的。
當多線程訪問容器裏不一樣段的數據時,線程間就不會存在鎖競爭,從而能夠有效提升併發訪問效率。安全

ConcurrentHashMap類圖
concurrentHashMap.png數據結構

初始化中除了initialCapacity,loadFactor參數,還有一個重要的concurrency level,它決定了segment數組的長度(默認是16,長度須要是2的N次方,與採用的哈希算法有關)。
每次get/put操做都會經過hash算法定位到一個segment,而後再經過hash算法定位到具體的entry。
get操做是不須要加鎖的,由於get方法裏將要使用的共享變量都定義成了volatile。多線程

定義成volatile的變量,可以在線程之間保持可見性,可以被多線程同時讀,而且保證不會讀到過時的值,可是隻能被單線程寫(有一種狀況能夠被多線程寫,就是寫入的值不依賴於原值,像直接set值就能夠,而i++這樣的操做就是非線程安全的)。

put方法在操做共享變量時必須加鎖,首先會定位到segment,而後在segment裏進行插入操做。
size方法,須要統計每一個segment中count變量的值,而後加和。可是咱們拿到的count值累加前可能已經發生了變化,那麼統計結果就不許確了。因此最安全的作法就是統計size的時候把全部segment的put,remove和clear方法所有鎖住,可是這種作法顯然很是低效。
ConcurrentHashMap的作法是先嚐試2次經過不鎖住segment的方式來統計各個segment大小,若是統計過程當中,容器的count發生了變化,再採用加鎖的方式統計全部segment的大小(put、remove和clear操做元素前都會將modCount進行加1,因此能夠經過在統計先後比較modCount是否發生變化來得知容器大小是否發生了變化)。

關於ConcurrentHashMap的迭代
使用了不一樣於傳統集合的快速失敗迭代器的另一種迭代方式,咱們稱爲弱一致迭代器。在這種迭代方式中,當iterator被建立後集合再發生改變就再也不是拋出ConcurrentModificationException,取而代之是在改變時new新的數據從而不影響原有的數據,iterator完成後再將頭指針替換爲新的數據,這樣iterator線程可使用原來老的數據,而寫線程也能夠併發的完成改變。更重要的,這保證了多個線程併發執行的連續性和擴展性,是性能提高的關鍵。

4. 拓展補充

4.1 LinkedList

HashMap使用了鏈表來存儲相同位置的Entry元素,接下來咱們參考jdk源碼實現一個簡化版的LinkedList,代碼以下:

/**
 * 手動實現一個鏈表
 * Date: 7/24/2016
 * Time: 3:45 PM
 *
 * @author xiaodong.fan
 */
public class MyLinkedList<E> implements Iterable<E> {

  int size = 0;
  int modCount = 0;
  Node<E> first;
  Node<E> last;

  /**
   * 添加元素
   * @param e
   */
  public void add(E e) {
    final Node<E> l = last;
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    if (l == null)
      first = newNode;
    else
      l.next = newNode;
    size++;
    modCount++;
  }

  /**
   * 移除元素
   * @param index
   * @return E
   */
  public E remove(int index) {
    checkElementIndex(index);

    Node<E> x = node(index);
    final E element = x.item;
    final Node<E> next = x.next;
    final Node<E> prev = x.prev;

    if (prev == null) {
      first = next;
    } else {
      prev.next = next;
      x.prev = null;
    }

    if (next == null) {
      last = prev;
    } else {
      next.prev = prev;
      x.next = null;
    }

    x.item = null;
    size--;
    modCount++;
    return element;
  }

  /**
   * 修改元素
   * @param index
   * @param element
   * @return E
   */
  public E set(int index, E element) {
    checkElementIndex(index);
    Node<E> x = node(index);
    E oldVal = x.item;
    x.item = element;
    modCount++;
    return oldVal;
  }

  /**
   * 獲取元素
   * @param index
   * @return E
   */
  public E get(int index) {
    checkElementIndex(index);
    return node(index).item;
  }

  /**
   * 迭代元素
   * @return Iterator<E>
   */
  @Override
  public Iterator<E> iterator() {
    return new Itr();
  }

  /*************************私有方法****************************/
  // 數據節點
  private static class Node<E> {
    E item;
    Node<E> next;
    Node<E> prev;

    Node(Node<E> prev, E element, Node<E> next) {
      this.item = element;
      this.next = next;
      this.prev = prev;
    }
  }

  // 快速失敗迭代器
  private class Itr implements Iterator<E> {
    // 迭代當前位置
    int cursor = 0;

    // 上一個迭代位置
    int lastRet = -1;

    // 迭代過程當中判斷是否有併發修改
    int expectedModCount = modCount;

    public boolean hasNext() {
      return cursor != size;
    }

    public E next() {
      checkForComodification();
      try {
        int i = cursor;
        E next = get(i);
        lastRet = i;
        cursor = i + 1;
        return next;
      } catch (IndexOutOfBoundsException e) {
        checkForComodification();
        throw new NoSuchElementException();
      }
    }

    public void remove() {
      if (lastRet < 0) {
        throw new IllegalStateException();
      }

      checkForComodification();
      try {
        MyLinkedList.this.remove(lastRet);
        if (lastRet < cursor) {
          cursor--;
        }
        lastRet = -1;
        expectedModCount = modCount;
      } catch (IndexOutOfBoundsException e) {
        throw new ConcurrentModificationException();
      }
    }

    final void checkForComodification() {
      if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
    }
  }

  private 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;
    }
  }

  private void checkElementIndex(int index) {
    if (!(index >= 0 && index < size))
      throw new IndexOutOfBoundsException("Index: "+index+", Size: "+size);
  }

}

4.2 實現LRU緩存

LRU是Least Recently Used 的縮寫,翻譯過來就是「最近最少使用」,LRU緩存就是使用這種原理實現,簡單的說就是緩存必定量的數據,當超過設定的閾值時就把一些過時的數據刪除掉。那怎麼肯定刪除哪條過時數據呢,採用LRU算法實現的話就是將最老的數據刪掉。
LinkedHashMap自身已經實現了順序存儲,默認狀況下是按照元素的添加順序存儲,也能夠啓用按照訪問順序存儲(指定構造函數第3個參數爲true便可),也就是最近讀取的數據放在最前面,最先讀取的數據放在最後面。它還有一個判斷是否刪除最老數據的方法,默認是返回false,即不刪除數據。因此咱們可使用LinkedHashMap很方便的實現LRU緩存。代碼以下:

/**
 * LRU緩存(當容量達到最大值時,刪除最近最少使用的記錄)
 * @param <K>
 * @param <V>
 */
public class LRULinkedHashMap<K, V> extends LinkedHashMap<K, V> {
  
  private static final long serialVersionUID = 1L;
  private final int maxCapacity;
  private static final float DEFAULT_LOAD_FACTOR = 0.75f;
  private final Lock lock = new ReentrantLock();

  public LRULinkedHashMap(int maxCapacity) {
    super(maxCapacity, DEFAULT_LOAD_FACTOR, true);
    this.maxCapacity = maxCapacity;
  }

  @Override
  protected boolean removeEldestEntry(java.util.Map.Entry<K, V> eldest) {
    return size() > maxCapacity;
  }

  @Override
  public V get(Object key) {
    try {
      lock.lock();
      return super.get(key);
    } finally {
      lock.unlock();
    }
  }

  @Override
  public V put(K key, V value) {
    try {
      lock.lock();
      return super.put(key, value);
    } finally {
      lock.unlock();
    }
  }

  @Override
  public int size() {
    try {
      lock.lock();
      return super.size();
    } finally {
      lock.unlock();
    }
  }
}

5. 參考文章

HashMap的工做原理
LRU緩存實現(Java)
Hashtable與ConcurrentHashMap區別
《java併發編程的藝術》迷你書

相關文章
相關標籤/搜索