2-1 HashMap-CurrentHashMap
java基础
java集合
JVM
多线程
mysql_数据库
计算机网络
nosql_redis
设计模式
操作系统
消息中间件activeMq
SSM框架面试题
服务中间件Dubbo
注意: 本文设计的原理和源码大部分都是jdk1.8的,不过我会给出1.7的源码。注意查看 我会标记清楚的。
1-HashMap
1-1原理
hashmap是数组+链表或红黑树(1.8新增)结合体,数组的每个元素存储的是链表的头结点。(1.8)向hashmap put值的时候,先取key.hashcode 在hash & length -1计算出数组下标。
if 该下标对应的链表为空,直接把键与值作为链表头节点。不为空,则遍历链表是否存在于key相同的节点,{ 相同,value值替换。 不相同,则放在尾部。1.8
}
1.7是头插法,而1.8是尾插法,待会我给你们解释为什么要这样做。
1-2hash函数的计算
1.8 直接上源码
1.8
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
下图为计算过程 ,对key。
1.7可不是这样的哦 没有 1.8简单,而且1.8更方便易懂。
1.7
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
1-3hashmap参数以及扩容机制
初始容量为 16, 达到阈值扩容, 阈值== 最大容量 * 负载因子(0.75),每次扩容后为原始容量的2倍。
扩容机制 resize()。 使用一个容量更大的数组来代替已有容量小的数组,transfer()方法将原有Entry数组的元素拷贝到新的Entry数组里,jdk1.7要重新计算元素在数组中的位置。 jdk1.8中不是重新计算而是采用了一种更巧妙的方式。
1-4get put源码1.7 1.8 都是没有枷锁
get put 1.8源码
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
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;
}
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
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查找
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; // 对hashmap的修改次数
// 是否需要扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
1-5Java8对hashmap的改进
优化后的底层结构: 数组+ 链表 + 红黑树。当链表长度 >= 8时, 链表转变为红黑树,利用红黑树的插入,删除,查找的算法。
java8 中对hashmap扩容不是重新计算所有元素在数组的位置,而是我们使用的是2次幂的扩展(指长度扩为原来的2倍), 所以, 元素的位置要么是在原来的位置,要么是在原位置再移动2次幂的位置。不需要向jdk1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了。要是0的话索引就没变,要是1的话索引变为" 原索引 + oldCap "。
1-6hashmap为什么可以插入空值?
hashmap中添加 key =null, 的entry 会调用putForNullKey()直接通过遍历 table[0].entry链表
{
有key相同,更新value,返回oldValue。 不相同,就调用addEntry 添加一个key=null的entry
}
1-7hashmap为什么线程不安全?
扩容导致的
jdk1.7扩容时,多个线程造成环形链表,在get出造成死循环+ 数据丢失。
因为两个线程A, B 同时调用AddEntry。获取头结点,并构造插入会发生覆盖操作。
jdk1.8 并发操作put时,会发生数据覆盖的问题。
1-8hashmap中的key可以为任意对象或数据类型吗?
可以为null,但不能是可变对象,如果对象中属性改变,那么hashcode可变,导致下次无法找到数据。
如果可变对象能保证在不改变哈希值情况下,可以改变其属性值。
2-ConcurrentHashMap
2-1-jdk1.7下
2-1-1原理
一个ConcurrentHashMap 维护了一个Segment数组,一个Segment维护了一个hashEntry 数组。
其中Segment继承于 ReentrantLock
ConcurrentHashMap 使用分段锁技术,将数据分成一段段的存储,然后给每一段一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问,实现真正的并发访问。
我们来看看源码是怎样定义的,Segment
static final class Segment<K,V> extends ReentrantLock implements Serializable {
Segment 继承了ReentrantLock,表明每个Segment 都可以当做一个锁。这样对每个segment中的数据需要同步操作的话都是使用每个Segment容器对象自身的锁来实现。只有对全局需要改变时锁定的是所有的segment。
2-1-2方法get,put ,remove
get()
public V get(Object key) {
Segment<K,V> s; // manually integrate access methods to reduce overhead
HashEntry<K,V>[] tab;
int h = hash(key);
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
(tab = s.table) != null) {
// 拿到指定的Segment ,遍历下面的hashentry节点下的 链表
for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
e != null; e = e.next) {
K k;
if ((k = e.key) == key || (e.hash == h && key.equals(k)))
return e.value;
}
}
return null;
}
put()
private int hash(Object k) {
int h = hashSeed;
if ((0 != h) && (k instanceof String)) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
// Spread bits to regularize both segment and index locations,
// using variant of single-word Wang/Jenkins hash.
h += (h << 15) ^ 0xffffcd7d;
h ^= (h >>> 10);
h += (h << 3);
h ^= (h >>> 6);
h += (h << 2) + (h << 14);
return h ^ (h >>> 16);
}
@SuppressWarnings("unchecked")
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
int hash = hash(key);
int j = (hash >>> segmentShift) & segmentMask;
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
s = ensureSegment(j);
return s.put(key, hash, value, false);
}
remove()
public V remove(Object key) {
int hash = hash(key);
Segment<K,V> s = segmentForHash(hash);
return s == null ? null : s.remove(key, hash, null);
}
/**
* {@inheritDoc}
*
* @throws NullPointerException if the specified key is null
*/
public boolean remove(Object key, Object value) {
int hash = hash(key);
Segment<K,V> s;
return value != null && (s = segmentForHash(hash)) != null &&
s.remove(key, hash, value) != null;
}
public int size() {
// Try a few times to get accurate count. On failure due to
// continuous async changes in table, resort to locking.
final Segment<K,V>[] segments = this.segments;
int size;
boolean overflow; // true if size overflows 32 bits
long sum; // sum of modCounts
long last = 0L; // previous sum
int retries = -1; // first iteration isn't retry
try {
for (;;) {
if (retries++ == RETRIES_BEFORE_LOCK) {
for (int j = 0; j < segments.length; ++j)
ensureSegment(j).lock(); // force creation
}
sum = 0L;
size = 0;
overflow = false;
for (int j = 0; j < segments.length; ++j) {
Segment<K,V> seg = segmentAt(segments, j);
if (seg != null) {
sum += seg.modCount;
int c = seg.count;
if (c < 0 || (size += c) < 0)
overflow = true;
}
}
if (sum == last)
break;
last = sum;
}
} finally {
if (retries > RETRIES_BEFORE_LOCK) {
for (int j = 0; j < segments.length; ++j)
segmentAt(segments, j).unlock();
}
}
return overflow ? Integer.MAX_VALUE : size;
}
2-2-jdk1.8及其以上
2-2-1原理
其中抛弃了原有的 Segment 分段锁,而采用了CAS + synchronized 来保证并发安全性。
大于 8 的时候才是链表转红黑树的阈值,当 table[i] 下面的链表长度大于·8时,就会转化为红黑树。
2-2-2-put操作--------重要哦
步骤:
根据 key 计算出 hashcode 。
判断是否需要进行初始化。
f 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。
如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。
如果都不满足,则利用 synchronized 锁写入数据(分为链表写入和红黑树写入)。
如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树。
public V put(K key, V value) {
return putVal(key, value, false);
}
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
// key 或者 value 抛出异常
if (key == null || value == null) throw new NullPointerException();
// 第一步,spread 会根据key的hashcode计算出hash值。
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
// 第二部,判读是否需要初始化
tab = initTable();
// 第三部,定位到,当前的node,判断是否为空 为空则新建节点
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
// 第四部,判断hash == MOVED 需要扩容了
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
// 第五步, 加锁了哦 ,分链表写入 和 红黑树写入
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
// 链表 转化为 红黑树 因为 数量 >= 8
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
//
addCount(1L, binCount);
return null;
}
spread
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
2-2-3get-size方法
根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。
如果是红黑树那就按照树的方式获取值。
就不满足那就按照链表的方式遍历获取值
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
while ((e = e.next
) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
public int size() {
long n = sumCount();
return ((n < 0L) ? 0 :
(n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
(int)n);
}