HashMap和ConcurrentHashMap
JDK1.7-HashMap
实现的数据结构:数组和链表 map[] + Entry<K,V>;
map[] 为数组:结构为数组;
Entry<K,V>为链表:结构为
{
String key; //key值
String value; //value 值
Entry<K,V> next; //下一个Entry
int hash; //key的hashcode值
}
数组默认初始化大小为16,最大的数组长度是1<<30,加载因子是0.75
线程不安全
/**
* map的put方法
*/
public V put(K key, V value) {
/**
* 这段代码执行,inflateTable(threshold) 会将table初始化为一个长度为16的Entry数组。
*/
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
/**
* key为null时,能够进行插入,所以HashMap支持为null的key
*/
if (key == null)
return putForNullKey(value);
/**
* 计算key值得hash值
*/
int hash = hash(key);
/**
* 通过key值得hash值,计算出map中数组得索引值
*/
int i = indexFor(hash, table.length);
/**
* 插入之前判断该key是否插入过,如果插入过,就将原value值覆盖,同时返回旧value值
*/
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
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++;
/**
* 插入新的entry值
*/
addEntry(hash, key, value, i);
return null;
}
int hash = hash(key); //计算出hash值
int i = indexFor(hash, table.length); //通过key值得hash值,计算出map中数组得索引值
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
static int indexFor(int h, int length) {
return h & (length-1);
}
两段代码详解:
1. hash(Object k):计算hash值,但是hash函数之后存在一系列得亦或运算,他的根本作用是增强散列性,使数据能够均匀得散列在map中,减少hash碰撞。
2.indexFor(int h, int length)原理:
hash值 & length - 1:
eg:
95=00000000000000000000000001011111
length: 16
15=00000000000000000000000000001111
&:当值都为1时,结果为1
95=00000000000000000000000001011111
&
15=00000000000000000000000000001111
=
15=00000000000000000000000000001111
所以结果由此可见只能通过最后四位来决定,因此结果一定会在[0-15]之间(长度为4,8,32的同理可得结果都在[0-3],[0-7],[0-31]):
同时需要满足这个条件的必然需求时map的数组容量必须是2的n方数;所以HashMap的size永远是2的n方数,代码实现如下:
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
private V putForNullKey(V value) {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(0, null, value, 0);
return null;
}
putForNullKey详解:
当key为null时,由代码可以得出,该entry值存放位置为数组得第一个位置,而且有且只有一个key为null值得entry,当key为null值得时候,会现遍历数组第一个位置得链表,如果有key为null得entry,就会进行更新,同时会返回旧值,当然未遍历到就进行插入。
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
addEntry详解:
加新的Entry会先判断当前插入是否会导致使用map的容量值 > size*0.75f(也就是扩容阈值),如果大于阈值,就是将map扩容至 2*oldSize,之后在进行插入操作;
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
createEntry详解:
获取计算的位置的链表,将entry.next指向链表首个entry,然后移位将该entry置为链表的首个entry:
table[bucketIndex] = new Entry<>(hash, key, value, e);
hashMap resize在多线程情况下会出现链表死锁的问题:
因为map在链表的插入时插入到第一个entry,当进行resize时,会导致链表倒排,当一个线程倒排时不会出现问题的,如果出现多个线程同时对同一个链表倒排,此时会出现链表死锁的情况。
JDK_1.7-ConcurrentHashMap
ConcurrentHashMap的出现主要是为了提供一个线程安全的Map,相对于HashMap,他的效率肯定会更低,但是他的优势时线程安全的;他的实现原理就是对map内的数组元素进行加锁(Segment分段锁),以此来保证线程安全性;
ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表,同时又是一个ReentrantLock(Segment继承了ReentrantLock);
ConcurrentHashMap使用分段锁技术,将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问,能够实现真正的并发访问

JDK_1.8-HashMap
JDK_1.8相比JDK_1.7有所优化:
①. hash()方法改变
②. 当链表长度大于8时,链表转换为红黑树,提高查询效率
③. 链表插入在链表尾部,jdk_1.7 插入在链表头部
hash()方法改变:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
解析:相较与JDK_1.7, 异或运算变少,散列性较低,如此做的原因是JDK_1.8加入了红黑树,提高了查询效率,所以把散列性也降低了。
红黑树
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)
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);
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;
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;
}
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);
}
}
链表插入在链表尾部:
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
解析: 当链表的next为空,说明这是链表末端。
链表插入在链表尾部解决了JDK_1.7的链表死锁问题;
为什么JDK_1.7链表需要在头端插入而不是末端?
解析:JDK_1.7从头部插入时因为插入速度快,而且不需要遍历整个链表。
插入时会遍历链表,当发现链表有key时会进行更新,那这样也相当于遍历过链表?
解析:插入时遍历可能会在遍历到第一个位置时就找到结果,之后就不会进行遍历,这样更新操作的效率会变快。
为什么JDK_1.8链表需要在末端插入?
解析:因为JDK_1.8本来就需要遍历链表来判断是否将链表转换为红黑树,而且最关键的是解决了链表死锁的问题。
来源:CSDN
作者:Yamhto
链接:https://blog.csdn.net/weixin_43433208/article/details/103496337