HashMap的实现原理:
HashMap 是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null键和null值。
当我们往HashMap中put元素时,首先根据key的hashcode 重新计算hash值,根据hash值得到这个元素在数组中的位置,如果该数组在该位置上已经存放了其他元素,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾,如果数组中该位置没有元素,就直接将该元素放在数组的该位置上。
需要注意jdk1.8 中对HashMap的实现做了优化,当链表中的节点数据超过8个之后,该链表会转为红黑树来提高查询效率,从原来的O(n) 到 O(logn)
简单实现:
public class MyHashmap<K,V> {
private final static Integer CAPACITY = 3;
private int size = 0;
private Entry<K, V>[] table = null;
public void put(K key, V value) {
if(table == null) {
inflate();
}
int hashCode = hash(key);
int index = indexFor(hashCode);
for(Entry<K, V> entry = table[index]; entry != null; entry = entry.next) {
if(entry.key.equals(key)) {
entry.value = value;
return;
}
}
addEntry(key, value, index);
}
private void addEntry(K key, V value, int index) {
Entry newEntry = new Entry(key, value, table[index]);
table[index] = newEntry;
size ++;
}
public V get(K key) {
int hashCode = hash(key);
int index = indexFor(hashCode);
for(Entry<K, V> entry = table[index]; entry != null;entry = entry.next) {
if(entry.key.equals(key)) {
return entry.value;
}
}
return null;
}
public int getSize() {
return size;
}
private int indexFor(int hashCode) {
return hashCode % table.length;
}
private int hash(K key) {
return key.hashCode();
}
private void inflate() {
table = new Entry[CAPACITY];
}
class Entry<K,V> {
K key;
V value;
Entry next;
public Entry(K key, V value, Entry next) {
this.key = key;
this.value = value;
this.next = next;
}
public K getKey() {
return key;
}
public V getValue() {
return value;
}
}
public static void main(String[] args) {
MyHashmap map = new MyHashmap();
map.put("k1", "v1");
map.put("k2", "v2");
map.put("k3", "v3");
map.put("k4", "v4");
map.put("k5", "v5");
map.put("k6", "v6");
System.out.println(map.get("k5"));
}
}
-
扩容机制:
transient Node<K,V>[] table;
当 map.size > threshold(键值对数组table.length * 加载因子)
table.length*2 -
为什么重写equals方法时 也要重写hashcode() ?
hashcode代表一个对象的签名,两个对象相等则hashcode一定要相等,
但是两个hashcode相等,对应的两个对象不一定相等(碰撞) -
concurrentHashMap
分段锁
ConcurrentHashMap
1.7 与 1.8的区别
JDK1.7的实现:
ConrruentHashMap由一个个Segment组成,简单来说,ConcurrentHashMap是一个Segment数组,它通过继承ReentrantLock来进行加锁,通过每次锁住一个segment来保证每个segment内的操作的线程安全性从而实现全局线程安全。
当每个操作分布在不同的segment上的时候,默认情况下,理论上可以同时支持16个线程的并发写入。
JDK 1.8
取消了segment分段设计,直接使用Node数组来保存数据,并且采用Node数组元素作为锁来实现每一行数据进行加锁来进一步减少并发冲突的概率
将原本数组+单向链表的数据结构变更为了数组+单向链表+红黑树的结构。
为什么要引入红黑树呢?在正常情况下,key hash之后如果能够很均匀的分散在数组中,那么table数组中的每个队列的长度主要为0或者1.但是实际情况下,还是会存在一些队列长度过长的情况。如果还采用单向列表方式,那么查询某个节点的时间复杂度就变为O(n); 因此对于队列长度超过8的列表,JDK1.8采用了红黑树的结构,那么查询的时间复杂度就会降低到O(logN),可以提升查找的性能
结构和JDK1.8版本中的Hashmap的实现结构基本一致,但是为了保证线程安全性,ConcurrentHashMap的实现会稍微复杂一下
put 方法
1. 根据key计算hash值
2. for自旋(整个添加逻辑)
int binCount = 0; // 节点数量:key冲突,node节点数量,当这个值大于8时 链表转为红黑树, 2:红黑树 0: 无冲突
private transient volatile int sizeCtl; // -1表示数组正初始化, > 0 表示下一次扩容时数组key数量(= 数组长度* 0.75)
private transient volatile CounterCell[] counterCells; // 计算map大小,用于分布式计算
private transient volatile long baseCount; // map大小
- tab = initTable(); //初始化数组,用到sizeCtl 的 cas操作 ,保证初始化一次
- tabAt(tab, i = (n - 1) & hash) // 获取key对应位置的value,这里根据 & 运算,计算下标位置,下标位置由 hash值的低位决定,这种计算方式的目的,是为后续扩容做兼容,数组扩容时低位下标不变,这样复制后仍能找到相应节点
- casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)) // 如果没有相应的key,则新增一个节点插入
- (fh = f.hash) == MOVED // 表示数组应该扩容了
- tab = helpTransfer(tab, f);
- 以下逻辑是 链表 + 红黑树的情况
synchronized (f) {} // 分布式锁,锁粒度是当前的节点
if (tabAt(tab, i) == f) // hash 冲突,
key.equals(ek) // key值相同,替换或不替换
if ((e = e.next) == null) { pred.next = new Node<K,V>(hash, key, value, null); break; } // key 值不相同则在链表末端新加入Node节点
else if (f instanceof TreeBin) // 红黑树
p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value) // 添加树节点
if (binCount != 0) if (binCount >= TREEIFY_THRESHOLD) // 链表长度 > 8 ,转红黑树
treeifyBin(tab, i);
3. 根据分布式锁 计算 map大小
addCount(1L, binCount); // 计算map中 key 的数量 + 1, 扩容
private transient volatile long baseCount;
private transient volatile CounterCell[] counterCells;
private transient volatile int cellsBusy;
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x))
// 先走一次 CAS计算baseCount值, 如果有冲突,则按counterCells 方式计算
// 整体CAS方式 性能下降(尝试一次,赋值baseCount)
// 设计:分而治之,数组计算,每个数组元素CAS
// 初始化counterCells
// cellsBusy 占位符,1表示有线程正初始化数组
else if (cellsBusy == 0 && counterCells == as &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
boolean init = false;
try { // Initialize table
if (counterCells == as) {
CounterCell[] rs = new CounterCell[2];
rs[h & 1] = new CounterCell(x);
counterCells = rs;
init = true;
}
} finally {
cellsBusy = 0;
}
if (init)
break;
}
hash算法与寻址算法优化
hash算法优化
key先计算hash值,然后对hashcode右移16位,异或
hash值是int类型,4字节32位,
让高低16位进行异或,低16位同时保持高低16位的特征,会尽量避免hash值的冲突
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
寻址算法优化
相比取模运算,与运算性能更高
i = (n - 1) & hash
hash冲突问题
多个key 经过hash算法 与 寻址算法 计算出来的数组下标位置相同,
此时就会出现hash冲突问题,在这一下标位置挂一个链表,通过遍历链表找到自己要找的哪个key-value
遍历链表的时间复杂度为O(n),
当链表的长度达到8,数组的长度达到64时,链表会转红黑树,时间复杂度为 O(logn)
如何扩容的?
底层是一个数组,当数组容量值达到 3/4时,会自动2倍扩容,变成了一个更大的数组,
rehash 重新计算下标位置,
本来在同一数组位置的key-value,扩容之后会分开
ConcurrentHashMap
jdk1.8以后,细粒度锁,对数组每个元素加锁
一个数组,每个元素进行CAS,如果成功直接更新成功,
如果失败说明 别的线程操作,此时 synchronized 对数组元素加锁,链表或红黑树处理