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
注:圖片源自http://www.admin10000.com/doc...算法
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的同步方法只能進入阻塞或輪詢狀態。緩存
核心:採用segment分段鎖來保護不一樣段的數據,是線程安全且高效的。
當多線程訪問容器裏不一樣段的數據時,線程間就不會存在鎖競爭,從而能夠有效提升併發訪問效率。安全
ConcurrentHashMap類圖數據結構
初始化中除了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線程可使用原來老的數據,而寫線程也能夠併發的完成改變。更重要的,這保證了多個線程併發執行的連續性和擴展性,是性能提高的關鍵。
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); } }
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(); } } }
HashMap的工做原理
LRU緩存實現(Java)
Hashtable與ConcurrentHashMap區別
《java併發編程的藝術》迷你書