带着问题看文章:
- ConcurrentHashMap是怎样实现线程安全的?
- 它和HashMap有什么异同呢?
- 在什么样的场景下用ConcurrentHashMap呢?
- 为什么HashMap和ConcurrentHashMap的链表数目>=8才转换成红黑树?为什么<=6才从树转换成链表?
put()方法入口:
本质上调用了putVal()方法
public V put(K key, V value) {
return putVal(key, value, false);
}
进入putVal()方法:
/**
*
* @param key 键
* @param value 值
* @param onlyIfAbsent 是否不替换原值?put方法默认为false,表示会替换原值
* @return 返回原值
*/
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
//计算hash值
int hash = spread(key.hashCode());
//这个参数后面用于判断链表和红黑树
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
//如果tab为null,初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
//如果当前索引位置没有值,直接创建
//tabAt的作用:final修饰,使用Unsafe类,计算放在槽的下标位置,返回一个Node节点
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//cas 在 i 位置创建新的元素,当 i 位置是空时,即能创建成功,结束for自循,否则继续自旋,casTabAt()里面调用了使用Unsafe类的compareAndSwapObject方法
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin:bin为空时使用了无锁的方式
}
//如果当前节点正在扩容,就会辅助扩容
//正在扩容的节点叫转移节点,它的 hash 值是固定的,都是 MOVED == -1
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
//当前槽点有值
else {
V oldVal = null;
//同步代码块,锁住当前节点f是前面定义的Node对象,锁对象,保证了安全(防止在高并发的场景下,多线程更改同一个数据带来的线程安全问题)
synchronized (f) {
//这里再次判断索引i位置的数据有没有被更改
if (tabAt(tab, i) == f) {
//如果当前节点是链表
if (fh >= 0) {
//记录链表中元素的个数
binCount = 1;
//循环遍历链表
for (Node<K,V> e = f;; ++binCount) {
K ek;
//如果key已经存在
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
//onlyIfAbsent = false,放入新值,返回旧值
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
//如果key不存在
//把新增的元素赋值到链表的最后(采用尾插法),然后break循环
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
//如果当前节点是红黑树节点
//TreeBin 是内部类,持有红黑树的引用,使用了volatile和Unsafel类的方法(使用CAS+park/unpark机制实现加锁和解锁),保证其操作的线程安全
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
//满足if的话,把老的值给oldVal
//在putTreeVal方法里面,在给红黑树重新着色旋转的时候会锁住红黑树的根节点
//putTreeVal方法:红黑树的put方法
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
//满足条件,新值替换旧值
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
//如果binCount有值
if (binCount != 0) {
//如果binCount >= 8,就把链表转换成红黑树
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
//如果oldVal有值,返回oldVal
if (oldVal != null)
return oldVal;
//break掉这个循环(注意这个循环是最外层的)
//这一步几乎走不到,因为槽点已经上锁,只有在红黑树或者链表新增失败的时候
//才会走到这里,但是这两者新增都是自旋的(相当于一直重试),几乎不会失败
break;
}
}
}
//检查是否需要扩容,addCount()内部维护了扩容的方案,如果需要扩容,就调用transfer 方法去扩容
addCount(1L, binCount);
return null;
}
put方法逻辑:
- 判断Node[]数组是否初始化,没有则进行初始化操作
- 通过hash定位数组的索引坐标,是否有Node节点,如果没有则使用CAS进行添加(链表的头节点),添加失败break掉,进入下一次循环
- 检查内部是否在扩容,如果在扩容就帮助其一起扩容
- 如果f!=null,则使用synchronized锁住f元素(链表/红黑二叉树的头元素)
4.1 如果是Node(链表结构)则执行链表的添加操作
4.2 如果是TreeNode(树形结构)则执行树的添加操作 - 判断链表长度是否达到临界值8,如果大于8就把链表转换成树结构
hash值得计算方式:
//转化为2进制为32个1
static final int HASH_BITS = 0x7fffffff;
/**
* 高16位和低16位异或再和HASH_BITS进行与运算
* @param h key的hashCode值
* @return hash值
*/
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
为什么要这么玩?为了增加散列度,使用了“位扰动”的方式,让元素更均匀的分布在有限的槽上
运算符^ &
的含义:与、或、异或运算
问题回答:
- ConcurrentHashMap是怎样实现线程安全的?
JDK8之前,使用的是分段锁实现的,将整个数组结构分为多段,每段加锁;在JDK8之后,锁粒度更细,取消分段锁,每个bucket都单独加锁(锁住首节点),采用CAS+Synchronized的方式
首先使用无锁操作CAS插入头节点,失败则循环重试
若头节点已存在,则尝试获取头节点的同步锁,再操作
- 它和HashMap有什么异同呢?
相同点:
数据结构和设计的方式基本相同,都采用了数组+链表/红黑树的结构
不同点:
ConcurrentHashMap的变量采用的volatile关键字修饰,保证多线程之间的可见性
ConcurrentHashMap添加sizeCtl关键字控制初始化和扩容,负数表示正在初始化或扩容(-1表示正在初始化,-(1+n)表示多少个线程在辅助扩容),0表示没有进行初始化,大于0表示下次扩容/初始化后容量的大小
HashMap可以有null值null键,ConcurrentHashMap不允许(HashTable也不可以),添加会报空指针异常
- 在什么样的场景下用ConcurrentHashMap呢?
- 为什么HashMap和ConcurrentHashMap的链表数目>=8才转换成红黑树?为什么<=6才从树转换成链表?
当hashCode离散性很好的时候,树型转化用到的概率非常小,因为数据均匀分布在每个桶中,几乎不会有桶中链表长度会达到阈值。但是在随机hashCode下,离散性可能会变差,然而JDK又不能阻止用户实现这种不好的hash算法,因此就可能导致不均匀的数据分布。不过理想情况下随机hashCode算法下所有桶中节点的分布频率会遵循泊松分布,我们可以看到,一个桶中链表长度达到8个元素的概率为0.00000006,几乎是不可能事件。所以,之所以选择8,不是拍拍屁股决定的,而是根据概率统计决定的。
后续方法请看这篇文章:
https://blog.youkuaiyun.com/weixin_44397907/article/details/115699102?spm=1001.2014.3001.5501