简单来讲,HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,若是定位到的数组位置不含链表(当前entry的next指向null),那么对于查找,添加等操做很快,仅需一次寻址便可;若是定位到的数组包含链表,对于添加操做,其时间复杂度依然为O(1),由于最新的Entry会插入链表头部,仅需简单改变引用链便可,而对于查找操做来说,此时就须要遍历链表,而后经过key对象的equals方法逐一比对查找。因此,性能考虑,HashMap中的链表出现越少,性能才会越好。算法
hash函数(对key的hashcode进一步进行计算以及二进制位的调整等来保证最终获取的存储位置尽可能分布均匀)数组
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}安全
查找函数数据结构
在JDK1.8 对hashmap作了改造,以下图并发
JDK 1.8 之前 HashMap 的实现是 数组+链表,即便哈希函数取得再好,也很难达到元素百分百均匀分布。函数
当 HashMap 中有大量的元素都存放到同一个桶中时,这个桶下有一条长长的链表,这个时候 HashMap 就至关于一个单链表,假如单链表有 n 个元素,遍历的时间复杂度就是 O(n),彻底失去了它的优点。性能
针对这种状况,JDK 1.8 中引入了 红黑树(查找时间复杂度为 O(logn))来优化这个问题优化
Hashtable它包括几个重要的成员变量:table, count, threshold, loadFactor, modCount。spa
put 方法的整个流程为:线程
public synchronized V put(K key, V value) { // Make sure the value is not null确保value不为null if (value == null) { throw new NullPointerException(); } // Makes sure the key is not already in the hashtable. //确保key不在hashtable中 //首先,经过hash方法计算key的哈希值,并计算得出index值,肯定其在table[]中的位置 //其次,迭代index索引位置的链表,若是该位置处的链表存在相同的key,则替换value,返回旧的value Entry tab[] = table; int hash = hash(key); int index = (hash & 0x7FFFFFFF) % tab.length; for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) { if ((e.hash == hash) && e.key.equals(key)) { V old = e.value; e.value = value; return old; } } modCount++; if (count >= threshold) { // Rehash the table if the threshold is exceeded //若是超过阀值,就进行rehash操做 rehash(); tab = table; hash = hash(key); index = (hash & 0x7FFFFFFF) % tab.length; } // Creates the new entry. //将值插入,返回的为null Entry<K,V> e = tab[index]; // 建立新的Entry节点,并将新的Entry插入Hashtable的index位置,并设置e为新的Entry的下一个元素 tab[index] = new Entry<>(hash, key, value, e); count++; return null; }复制代码
相比较于 put 方法,get 方法则简单不少。其过程就是首先经过 hash()方法求得 key 的哈希值,而后根据 hash 值获得 index 索引(上述两步所用的算法与 put 方法都相同)。而后迭代链表,返回匹配的 key 的对应的 value;找不到则返回 null。
public synchronized V get(Object key) { Entry tab[] = table; int hash = hash(key); int index = (hash & 0x7FFFFFFF) % tab.length; for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) { if ((e.hash == hash) && e.key.equals(key)) { return e.value; } } return null; }复制代码
在JDK1.7版本中,ConcurrentHashMap的数据结构是由一个Segment数组和多个HashEntry组成
Segment数组的意义就是将一个大的table分割成多个小的table来进行加锁,也就是上面的提到的锁分离技术,而每个Segment元素存储的是HashEntry数组+链表,这个和HashMap的数据存储结构同样
对于ConcurrentHashMap的数据插入,这里要进行两次Hash去定位数据的存储位置
static
class
Segment<K,V>
extends
ReentrantLock
implements
Serializable {
从上Segment的继承体系能够看出,Segment实现了ReentrantLock,也就带有锁的功能,当执行put操做时,会进行第一次key的hash来定位Segment的位置,若是该Segment尚未初始化,即经过CAS操做进行赋值,而后进行第二次hash操做,找到相应的HashEntry的位置,这里会利用继承过来的锁的特性,在将数据插入指定的HashEntry位置时(链表的尾端),会经过继承ReentrantLock的tryLock()方法尝试去获取锁,若是获取成功就直接插入相应的位置,若是已经有线程获取该Segment的锁,那当前线程会以自旋的方式去继续的调用tryLock()方法去获取锁,超过指定次数就挂起,等待唤醒。
ConcurrentHashMap的get操做跟HashMap相似,只是ConcurrentHashMap第一次须要通过一次hash定位到Segment的位置,而后再hash定位到指定的HashEntry,遍历该HashEntry下的链表进行对比,成功就返回,不成功就返回null。
计算ConcurrentHashMap的元素大小是一个有趣的问题,由于他是并发操做的,就是在你计算size的时候,他还在并发的插入数据,可能会致使你计算出来的size和你实际的size有相差(在你return size的时候,插入了多个数据),要解决这个问题,JDK1.7版本用两种方案。
JDK1.8的实现已经摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操做,整个看起来就像是优化过且线程安全的HashMap,虽然在JDK1.8中还能看到Segment的数据结构,可是已经简化了属性,只是为了兼容旧版本。
在上面的例子中咱们新增我的信息会调用put方法,咱们来看下。
咱们如今要回到开始的例子中,咱们对我的信息进行了新增以后,咱们要获取所新增的信息,使用String name = map.get(“name”)获取新增的name信息,如今咱们依旧用debug的方式来分析下ConcurrentHashMap的获取方法get()
public
V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p;
int
n, eh; K ek;
int
h = spread(key.hashCode());
//计算两次hash
if
((tab = table) !=
null
&& (n = tab.length) >
0
&&
(e = tabAt(tab, (n -
1
) & h)) !=
null
) {
//读取首节点的Node元素
if
((eh = e.hash) == h) {
//若是该节点就是首节点就返回
if
((ek = e.key) == key || (ek !=
null
&& key.equals(ek)))
return
e.val;
}
//hash值为负值表示正在扩容,这个时候查的是ForwardingNode的find方法来定位到nextTable来
//查找,查找到就返回
else
if
(eh <
0
)
return
(p = e.find(h, key)) !=
null
? p.val :
null
;
while
((e = e.next) !=
null
) {
//既不是首节点也不是ForwardingNode,那就往下遍历
if
(e.hash == h &&
((ek = e.key) == key || (ek !=
null
&& key.equals(ek))))
return
e.val;
}
}
return
null
;
}
其实能够看出JDK1.8版本的ConcurrentHashMap的数据结构已经接近HashMap,相对而言,ConcurrentHashMap只是增长了同步的操做来控制并发,从JDK1.7版本的ReentrantLock+Segment+HashEntry,到JDK1.8版本中synchronized+CAS+HashEntry+红黑树,相对而言,总结以下思考:
Hashtable和HashMap有几个主要的不一样:线程安全以及速度。仅在你须要彻底的线程安全的时候使用Hashtable,而若是你使用Java 5或以上的话,请使用ConcurrentHashMap吧。