HashMap原理以及源码详解
hashMap有一个默认的初始容量16,通过位运算,最大容量是2的20次幂
如果说写了初始容量11,容量就是11吗?
并不是11
hashMap的get、put操作时间复杂度是O(1)
hashMap并不是用取模计算index,而是用位运算,因为位运算效率远远高于取模,位运算最接近计算机运算
hash(key)是为了让数据在数组上分别的更加均匀一下
hash扩容,有个加载因子loadfactor=0.75,为什么?
时间和空间取了一个平衡,最好是通过牛顿二项式得出0.68,但是java中定位0.75
红黑树:当链表长度大于8后转为红黑树,当容量小于64时优先扩容
为什么链表长度大于8时链表转红黑树-在加载因子为0.75的情况下泊松分布(概率统计)千万分之六
在数据量大的情况下1.8比1.7提升5%-10%,在数据量小的情况下可能1.7比1.8性能还高
Java7版本的(数组+链表)链表是头部插入法
多线程情况下会出现链表成环(也就是死环)
初始化方法
public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) 初始化容量为0抛异常 throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY)如果初始化容量大于整型最大值,就把容量赋值为最大值 initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) 加载因子不能小于0或者等于null throw new IllegalArgumentException("Illegal load factor: " + loadFactor); // Find a power of 2 >= initialCapacity int capacity = 1; 初始化容量 while (capacity < initialCapacity) 如果初始化的容量小于给定的值,就乘以2,直到大于给定值跳出循环 capacity <<= 1; this.loadFactor = loadFactor; threshold = (int)(capacity * loadFactor);扩容的阈值 table = new Entry[capacity];初始化这个Entry数组 init(); }
现在看一下put方法
public V put(K key, V value) {
if (key == null) key如果为null,不用hash直接放到数组的第一位
return putForNullKey(value);
int hash = hash(key.hashCode());计算hashCode,这个hash方法中有位运算,为了使得key在Entry中更加散列,不至于链表过长
int i = indexFor(hash, table.length);通过&运算计算出这个key在数组中的位置
for (Entry<K,V> e = table[i]; e != null; e = e.next) {判断是否有重复的key,如果有就返回旧值,用新值覆盖旧值
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;记录hashMap修改的次数
addEntry(hash, key, value, i);添加值
return null;
}
添加值
void addEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex];取出当前数组索引的值 table[bucketIndex] = new Entry<K,V>(hash, key, value, e);新建一个Entry节点赋值给当前索引,把之前的索引位的值置为这个新的节点的next节点,构建一个链表结构,这也是hashMap的头部插入法 if (size++ >= threshold)当数组的容量大于等于hashMap的扩容阈值后就进行扩容 resize(2 * table.length);扩容的话数组容量变为原来的两倍,保证容量是2的指数次幂 }
扩容
void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } Entry[] newTable = new Entry[newCapacity]; transfer(newTable);扩容时进行数据转移,把当前的数组数据转移到新的数组中 table = newTable;扩容后把新的数组赋值给table threshold = (int)(newCapacity * loadFactor);重新计算扩容阈值 }
数据转移-单线程不会产生闭环死锁,但是在多线程下就有可能会产生闭环死锁
void transfer(Entry[] newTable) { Entry[] src = table; int newCapacity = newTable.length; for (int j = 0; j < src.length; j++) {遍历这个旧数组 Entry<K,V> e = src[j]; if (e != null) { src[j] = null; do {如果有链表这里循环取出链表上的数据 Entry<K,V> next = e.next;
有的版本这里还有一个rehash,重新计算hash值,但是我这个java7版本没有这行代码 int i = indexFor(e.hash, newCapacity);重新计算索引值 e.next = newTable[i];重新构建链表 newTable[i] = e;这个还是采用头部插入法,这样的话旧的链表的数据转移到新的数组中这个链表的顺序证号反过来了,每次扩容这个顺序都会发生变化 e = next; } while (e != null); } } }
Java8版本的(数组+链表+红黑树)
Put方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0)第一次初始化map 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;判断数组中是否有重复的key 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);创建一个节点,并把这个节点挂到p节点下 if (binCount >= TREEIFY_THRESHOLD - 1)链表长度大于7就变为红黑树结构 treeifyBin(tab, hash); break; }
判断链表中有没有重复的key if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e;把下一个节点复制给当前这个p,下次循环的时候就是循环的p.next节点 } } if (e != null) { e不为空说明有重复的key,用新值覆盖旧值,并把旧值返回// 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; }
扩容resize(),java8扩容时通过高低位来实现的,避免了多线程下java7的链表成环导致死锁的问题
Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { next = e.next; if ((e.hash & oldCap) == 0) {通过hash值&oldCap,计算的结果只有两种要么是16要么是0,如果为0就是低位,否则就是高位 if (loTail == null)第一次进来是loHead和loTail都指向同一个节点,第二次就把当前节点挂到第一个节点的后面,loTail指向当前节点 loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) {循环结束后loTail不为空的话就把loHead指向新的数组的索引的位置,把loTail.next 值为空 loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; }
线程安全ConcurrentHashMap解读
写同步,读无锁
1.7版本是通过分段锁保证写同步,减少竞争的冲突,hashTable是整个全锁
对象在初始化的时候就会初始化一个segment数组,大小默认为16,初始化segment后还需要创建一个HashEntry的数组,大小默认为整个hashMap的大小除以segment的大小
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (concurrencyLevel > MAX_SEGMENTS)
concurrencyLevel = MAX_SEGMENTS;
// Find power-of-two sizes best matching arguments
int sshift = 0;
int ssize = 1;
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
segmentShift = 32 - sshift;
segmentMask = ssize - 1;
this.segments = Segment.newArray(ssize);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity)
++c;
int cap = 1;
while (cap < c)
cap <<= 1;
for (int i = 0; i < this.segments.length; ++i)
this.segments[i] = new Segment<K,V>(cap, loadFactor);
}
put方法
put方法是对这个segment加锁,通过reentrentLock加锁,锁的粒度是整个segment,segment下面的hashEntry数组都需要加锁,还有扩容的话只是对hashEntry进行,源码中没有对segment进行扩容,扩容的时候采用头部插入法把数据重新放到扩容后的新的hashEntry数组中(1.7的扩容看源码)
1.8版本
1.初始化hash表,通过分段锁(sychnoized关键字实现)+cas来保证并发时数据的完整型,扩容的话,如果是多个处理器,并且在并发情况先就会多线程一起扩容,每个线程处理16个槽位,链表和红黑树都是通过高低位的方式从旧的数组中向新的数组转移数据(1.8的扩容看源码)。扩容时并发执行的,并加快了扩容的速度,同时不至于堵塞线程。
ConcurrentHashMap没有用ConcurrentModificationException去保护,也就是说在遍历的时候put,remove操作不会报ConcurrentModificationException错误,但是hashMap会报这个错误,因为ConcurrentHashMap的put、remove操作有同步机制,并且ConcurrentHashMap的源码中没有比较modCount和期望的修改值,因为util包中的迭代器实现是fast-failed迭代器,说白了就是一旦由修改就抛异常,在current包中迭代器是弱一致性迭代器,原来两种迭代器情况不一样