由于HashMap是非线程安全的,扩容时容易造成环形链表,导致在获取链表中不存在的值时陷入死循环。于是在HashMap在并发环境中需要加锁使用,这种方式开销大。Java中提供了另一种数据结构ConcurrentHashMap,专门用于并发环境下的Map映射需求。
具体机制:CAS 结合 synchronized,并通过sun.misc.Unsafe提供的可见性操作接口来访问数据。
主要关注以下几个地方:
- put 放数据
- resize 扩容
- get 取数据
1、对于 put 操作
key值求hash后所在table中索引位置无节点(null),通过CAS方式将节点插入到该索引位置
key值求hash后所在table中索引位置有节点(不为null),通过synchronized锁住该索引位置处链表或者红黑树的第一个节点
2、对于 resize 扩容操作
多线程都能参与扩容操作,多线程同步怎么做的?
1)sizeCtl :该标签用于保证resize调整不会重叠,在初始化和resize扩容的时候,sizeCtl负值。其中 -1:表示table正在初始化 -(1+活跃的扩容线程数):表示正在扩容。其它也有默认值、初始化表时的表的size值、或者下一次扩容阈值。
每个线程在帮助扩容时,都会通过CAS来对sizeCtl进行+1操作,表示增加了一个线程帮助扩容
2)ForwardingNode:标记节点,用来进行并发控制,对旧table中标记为ForwardingNode的节点不再处理,避免重复。
3)transferIndex:扩容时旧table中下一个需要转移的一个长度单位的起始下标,不管哪个线程一次任务中就处理 MIN_TRANSFER_STRIDE 这么多个。
先来看看怎么确定某个元素put后到达table中的下标位置
i = (n - 1) & hash
n 表示 table 长度,hash 表示插入元素的哈希值,这么做是因为 n 是 2 的幂,n-1 其二进制表示中各位均为1。
(n - 1) & hash 与 hash % n 取余的效果相同。这样便将 hash 值分布到 table 中。
transfer中有一段代码不好理解,这里是将旧表中的元素移到新表中去
if (fh >= 0) {
// fh & (n - 1) 决定了f节点在table中的下标,因为n是2的幂,
// 所以 fh & n 只有两个结果,要么是 0,要么是 n
// 所以这里是为了将该链表拆分成两个部分,结果为0的为一部分,为n的为另一部分
// 扩容之后数组长度增加一倍,数组元素要重新分布
// 最好的情况当然是原长度部分和扩容长度部分的元素各一半,并且尽多保持原来的顺序
int runBit = fh & n;
Node<K,V> lastRun = f;
// 从链表的第一个节点开始,遍历链表
for (Node<K,V> p = f.next; p != null; p = p.next) {
// 前边说过 p.hash & n 要么为0,要么为n
// 所以b的结果可能会出现很过个..0.0..n.n..0..n.0这样的情况
int b = p.hash & n;
if (b != runBit) {
// 于是 lastRun 便记录下了最后一个b值发生变化的节点
// 这样的目的在于,将放置到扩容长度部分中去的元素的链表从lastRun节点开始
// 也就是从后往前(头插方式)
// 还有一点需要关注,如果连续node的b值均为n或者0,那这连续的几个节点是不用一个一个搬运的
// 因为他们在新table中的位置是一样的,并且其next关系不变
// 所以搬运时只需要搬这几个连续节点中的第一个就行了
// 但是从后文看并不是这样的,后边只是对从lastRun节点开始的连续节点不用全部遍历,
// 其他节点还是一个一个处理的,那假如lastRun节点之前有好长一串其b值都连续相同,
// 那一个一个处理岂不是比较愚蠢?
runBit = b;
lastRun = p;
}
}
if (runBit == 0) {
ln = lastRun;
hn = null;
}
else {
hn = lastRun;
ln = null;
}
for (Node<K,V> p = f; p != lastRun; p = p.next) {
int ph = p.hash; K pk = p.key; V pv = p.val;
if ((ph & n) == 0)
// 符合这一条件的节点放在原长度部分对应下标i中,并且上一个节点都作为下一个节点的next
ln = new Node<K,V>(ph, pk, pv, ln);
else
// 符合这一条件的节点放在扩容长度部分对应下标 n+i 中,并且上一个节点都作为下一个节点的next
hn = new Node<K,V>(ph, pk, pv, hn);
}
// 设置扩容后的table中 i (原长度部分对应位置)和 i+n (扩容部分对应位置)
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
// 设置该节点已经成功转移,其他线程在帮助扩容时就不需要再处理该位置节点
setTabAt(tab, i, fwd);
advance = true;
}
线程锁住的是table中下标 i 处链表的第一个节点元素,配合可见性访问方法setTabAt对新表对应位置进行设置。
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}
3、对于get操作
直接通过可见性访问接口拿到table中指定位置的链表,遍历链表比较key值即可
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}