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并发编程的艺术》迷你书

相关文章
相关标签/搜索