HashMap源码分析
HashMap底层基于红黑树+hash表;extends AbstractMap,实现了clonable和序列化接口
有两个参数影响性能:负载因子和初始容量
static final float DEFAULT_LOAD_FACTOR = 0.75f;
负载因子,默认为0.75;经过科学计算的值;如果大于0.75会增加哈希表的利用效率,但是会增大哈希冲突的概率;小于0.75会减少哈希冲突,但是会降低hash表的利用效率
static final int TREEIFY_THRESHOLD = 8;
树化阈值,当某个桶节点下的链表长度超过8就将链表树化
static final int UNTREEIFY_THRESHOLD = 6;
解树化阈值,当红黑树在进行了扩容或者删除操作后个数<=6的时候,在下一次resize操作的时候将红黑树退化为链表,节省空间。
size是当前集合元素个数
默认容量:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; =16
int threshold=cap*加载因子;(达到这个值就开始扩容,第一次为12)
构造方法四个:
1)创建一个hashMap默认初始化容量为16,加载因子为0.75,其他属性默认
2)传入一个int型的初始化容量
3)传入一个float型的加载因子和int型初始化容量
4)(这个比较难,我自己是简单化理解了,具体实现还是要结合源码)传入一个任意的Map对象,将它变为HashMap
内部结构:
1.transient Node<K,V>[] table; Node是单向链表的一个节点,是HashMap中的一个静态内部类,实现了MapEntry接口,单向链表的一个节点,Node节点的hashcode值是key和value分别的hashCode值的异或。
2.TreeNode<K,V> 继承LinkedHashMap.Entry,红黑树是一种特殊的二叉查找树,红黑树的每个节点上都有存储位表示节点的颜色,可以是红或黑。
红黑树的特征:
1)根节点是黑节点;
2)每个节点不是黑节点就是红节点
3)如果一个节点是红节点,那么他的子节点必须为黑节点
4)每个为(null或NIL)的叶子节点是黑色。
5)是平衡二叉树。
6)左子树上所有结点的值都小于或者等于他的根节点的值;右子树上所有的值都大于或等于他根节点的值。所以左右子树都为二叉搜索(排序)树。
7)从任意节点到其每个叶子的所有路径都包含相同数目的黑色节点。
主要用红黑树来存储有序数据,时间复杂度为O(lgn)效率很高。
内部hash实现:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
插入数据(主要实现在putVal方法中)
传进一个key值,若key值为null就把他放在第一个位置,否则保留高16位,将他的高十六位与他的hashCode值进行亦或运算,因为hashCode值比较大(值越大,越高位就是有效位),异或操作是让高低十六位打乱,减少hash冲突。hash计算是为了计算找到对应的桶下标。不直接用Object类的hashCode是因为计算出来的hashcode值太大了,需要开辟大量空间并且很多都是无用的。
hash表通过key的hashcode%n取余就是桶数组中所在的位置
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//定义临时Node节点数组,p是任意节点变量,n是节点数组长度,i是索引位置
Node<K,V>[] tab; Node<K,V> p; int n, i;
//HashMap也是采用懒加载模式,此时还没有初始化,进行初始化操作
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;//第一次扩容时桶的数量为默认容量16
//此时hash表对应的下标还没有存储元素,i=(n-1)&hash)真正数组下标的计算;相当于取模操作
//n一定是2的次方,n-1的二进制码就一定是全1,这样保证hash表中的所有索引都有可能被访问到
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
//此时冲突位置的key和要保存元素的key值相等,把已经存在的值赋给临时变量e
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);
//链表长度>=7在下一次存储前树化;因为是++binCount
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
//遍历桶中的链表,与前面的e=p.next进行组合,作为遍历链表的条件
p = e;
}
}
/**如果在桶中找到了key、hash值都相等的结点,新值替换旧值
**/
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;
}
树化操作
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//此时要进行扩容
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
//保证每个节点都能访问到
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
扩容操作:
resize() 初始化或者对桶的大小进行扩容,为null就按threshold=16来进行分配,每次扩容为原来的2倍在hashMap中的键值对大于阈值或者初始化时,调用resize方法进行扩容,都是扩展2倍,扩展后Node对象的位置要么在原位置要么移动到偏移量两倍的位置。
1)hashMap初始化采用懒加载模式,在第一次添加元素的时候进行初始化,第一次调用put方法时才会真正地为数组分配空间。
2)桶数组中每个元素都是一个链表或红黑树,数组初始化长度是16,把数组中每一格称为一个桶,当数组中已经被使用的桶的数量超过了threshold时,就要进行扩容。
3)每个桶中都是一个链表,当链表长度超过树化阈值就要进行树化,红黑树长度小于解树化阈值就退化为链表,节省空间。
获取数据get(Object key)
主要由getNode()实现
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//查询下一个节点
if ((e = first.next) != null) {
//是红黑树节点
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
//否则就在链表中查找。
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
通过key值计算出当初存放的位置,然后从第一项开始找,如果没有找到,则通过f((e=first.next)!=null)查询下一个节点,如果是红黑树节点,就在红黑树中找,否则就在链表中查找。
HashMap根据value怎么找到key?
方法1:自定义一个getKey方法,传进来一个map和一个value;定义一个list(因为HashMap值不唯一;重复的key放在一个集合中)然后for循环遍历keySet,使用equals方法判断如果key.get(key)和value相等,就将当前key值放入list中。
方法二:将map中的每个键值对通过keySet变成set集合的对象;使用迭代器遍历;如果entry.getvalue().equals(value)则加入list中。
JDK1.7与JDK1.8中ConcurrentHashMap设计的区别与如何高效的实现线程安全
1.JDK1.7使用Lock体系中的ReentrantLock来保证线程安全,将hashtable中一把锁锁整张表优化为Segement(分段锁,16把锁,每把锁锁的是桶对象)
2.JDK1.8将锁的粒度进一步细化,每个桶的头结点加一把锁,底层基于红黑树和hash表,结构类似于HashMap,锁的数量会随着hash表的增加而增加,支持并发线程数进一步提高;使用Synchronized+CAS操作来保证线程安全。数据存储使用volatile保证可见性。内部虽然还有Segment的定义,但是仅仅是为了保证序列化时的兼容性,不再有任何结构上的用处,因为不使用Segment,初始化操作大大监护,修改为lazy-load形式,避免初始开销。
不同的segment是同步还是异步?大的Segement是不能扩容的,下面的每个子Segement可以扩容。