总的来讲,HashMap就是数组+链表的组合实现,每一个数组元素存储一个链表的头结点,本质上来讲是哈希表“拉链法”的实现。java
HashMap的链表元素对应的是一个静态内部类Entry,Entry主要包含key,value,next三个元素面试
在Jdk1.8中HashMap的实现方式作了一些改变,可是基本思想仍是没有变得,只是在一些地方作了优化,下面来看一下这些改变的地方,数据结构的存储由数组+链表的方式,变化为数组+链表+红黑树的存储方式,在性能上进一步获得提高。算法
HashMap 采用一种所谓的“Hash 算法”来决定每一个元素的存储位置。当程序执行 map.put(String,Obect)方法 时,系统将调用String的 hashCode() 方法获得其 hashCode 值——每一个 Java 对象都有 hashCode() 方法,均可经过该方法得到它的 hashCode 值。获得这个对象的 hashCode 值以后,系统会根据该 hashCode 值来决定该元素的存储位置数组
put方法分析:安全
public V put(K key, V value) { //调用putVal()方法完成 return putVal(hash(key), key, value, false, true); } final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; //判断table是否初始化,不然初始化操做 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //计算存储的索引位置,若是没有元素,直接赋值 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; //节点若已经存在,执行赋值操做 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //判断链表是不是红黑树 else if (p instanceof TreeNode) //红黑树对象操做 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { //为链表, for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); //链表长度8,将链表转化为红黑树存储 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } //key存在,直接覆盖 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } //记录修改次数 ++modCount; //判断是否须要扩容 if (++size > threshold) resize(); //空操做 afterNodeInsertion(evict); return null; }
下面将这个过程总结一下:数据结构
一、计算key的hash值,算出元素在底层数组中的下标位置。多线程
二、经过下标位置定位到底层数组里的元素(也有多是链表也有多是树)。并发
三、取到元素,判断放入元素的key是否==或equals当前位置的key,成立则替换value值,返回旧值。app
四、若是是树,循环树中的节点,判断放入元素的key是否==或equals节点的key,成立则替换树里的value,并返回旧值,不成立就添加到树里。函数
五、不然就顺着元素的链表结构循环节点,判断放入元素的key是否==或equals节点的key,成立则替换链表里value,并返回旧值,找不到就添加到链表的最后。
精简一下,判断放入HashMap中的元素要不要替换当前节点的元素,key知足如下两个条件便可替换:
一、hash值相等。
二、==或equals的结果为true。
String, Interger这样的类做为HashMap的键是再适合不过了,并且String最为经常使用。
由于String对象是不可变的,并且已经重写了equals()和hashCode()方法了。
1.不可变性是必要的,由于为了要计算hashCode(),就要防止键值改变,若是键值在放入时和获取时返回不一样的hashcode的话,那么就不能从HashMap中找到你想要的对象。不可变性还有其余的优势如线程安全。
2.由于获取对象的时候要用到equals()和hashCode()方法,那么键对象正确的重写这两个方法是很是重要的。若是两个不相等的对象返回不一样的hashcode的话,那么碰撞的概率就会小些,这样就能提升HashMap的性能。
Hashtable能够看作是线程安全版的HashMap,二者几乎“等价”(固然仍是有不少不一样)。Hashtable几乎在每一个方法上都加上synchronized(同步锁),实现线程安全。
区别
1.HashMap继承于AbstractMap,而Hashtable继承于Dictionary;
2.线程安全不一样。Hashtable的几乎全部函数都是同步的,即它是线程安全的,支持多线程。而HashMap的函数则是非同步的,它不是线程安全的。若要在多线程中使用HashMap,须要咱们额外的进行同步处理;
3.null值。HashMap的key、value均可觉得null。Hashtable的key、value都不能够为null;
4.迭代器(Iterator)。HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。因此当有其它线程改变了HashMap的结构(增长或者移除元素),将会抛出ConcurrentModificationException。
5.容量的初始值和增长方式都不同:HashMap默认的容量大小是16;增长容量时,每次将容量变为“原始容量x2”。Hashtable默认的容量大小是11;增长容量时,每次将容量变为“原始容量x2 + 1”;
6.添加key-value时的hash值算法不一样:HashMap添加元素时,是使用自定义的哈希算法。Hashtable没有自定义哈希算法,而直接采用的key的hashCode()。
7.速度。因为Hashtable是线程安全的也是synchronized,因此在单线程环境下它比HashMap要慢。若是你不须要同步,只须要单一线程,那么使用HashMap性能要好过Hashtable。
可否让HashMap同步?
HashMap能够经过下面的语句进行同步:Map m = Collections.synchronizeMap(hashMap);
HashTable容器在竞争激烈的并发环境下表现出效率低下的缘由,是由于全部访问HashTable的线程都必须竞争同一把锁。
那假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不一样数据段的数据时,线程间就不会存在锁竞争,从而能够有效的提升并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术。
首先将数据分红一段一段的存储,而后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其余段的数据也能被其余线程访问。有些方法须要跨段,好比size()和containsValue(),它们可能须要锁定整个表而而不只仅是某个段,这须要按顺序锁定全部段,操做完毕后,又按顺序释放全部段的锁。
这里“按顺序”是很重要的,不然极有可能出现死锁,在ConcurrentHashMap内部,段数组是final的,而且其成员变量实际上也是final的,可是,仅仅是将数组声明为final的并不保证数组成员也是final的,这须要实现上的保证。这能够确保不会出现死锁,由于得到锁的顺序是固定的。
ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。
Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap相似,是一种数组和链表结构, 一个Segment里包含一个HashEntry数组,每一个HashEntry是一个链表结构的元素, 每一个Segment守护者一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先得到它对应的Segment锁。
ConcurrentHashMap取消了segment分段锁,而采用CAS和synchronized来保证并发安全。数据结构跟HashMap1.8的结构同样,数组+链表/红黑二叉树。
synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提高N倍。
JDK1.8的ConcurrentHashMap的结构图以下:
TreeBin: 红黑二叉树节点Node: 链表节点