初始化
ConcurrentHashMap初始化设置容量时,底层会自动转换为距设置值2倍的最近的2的次幂;
【前后距离相等时选择向后转换:如3 * 2 = 6距离4和8相等,选择向后转换为8】
jdk8的版本实际上最多可以同时扩容的线程数是:hash桶的个数/16
get方法
get方法利用了volatile特性,实现了无锁读。
查找value的过程如下:
根据key定位hash桶,通过tabAt的volatile读,获取hash桶的头结点。
通过头结点Node的volatile属性next,遍历Node链表
找到目标node后,读取Node的volatile属性val
可见上述3个操作都是volatile读,因此可以做到在不加锁的情况下,保证value的内存可见性
put方法
jdk8的实现中,锁的粒度是hash桶,因此对table数组元素的读写,大部分都是在没有锁的保护下进行的
ConcurrentHashMap中的锁是hash桶的头结点,那么当多个put线程访问头结点为空的hash桶时,在没有互斥锁保护的情况下,多个put线程都会尝试将元素插入头结点,此时如何确保并发安全呢?
要解决这个问题,我们要先了解什么是CAS操作:CAS,即compareAndSwap,原子的执行比较并交换的操作:当前内存值如果和预期值相等,则将其更新为目标值,否则不更新。
然后,我们看下ConcurrentHashMap的put方法是如何通过CAS确保线程安全的:
假设此时有2个put线程,都发现此时桶为空,线程一执行casTabAt(tab,i,null,node1),此时tab[i]等于预期值null,因此会插入node1。随后线程二执行casTabAt(tba,i,null,node2),此时tab[i]不等于预期值null,插入失败。然后线程二会回到for循环开始处,重新获取tab[i]作为预期值,重复上述逻辑。
final V putVal(K key, V value, boolean onlyIfAbsent) {
...
for (Node<K,V>[] tab = table;;) {
...
//key定位到的hash桶为空
if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//cas设置tab[i]的头结点。
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; //设置成功,跳出for循环
//设置失败,说明tab[i]已经被另一个线程修改了。回到for循环开始处,重新判断hash桶是否为空。如何往复,直到设置成功,或者hash桶不空。
}else{
synchronized (f) {
//
}
}
}
...
}
以上通过for循环+CAS操作,实现并发安全的方式就是无锁算法(lock free)的经典实现
//JDK7版本的 AtomicInteger 类的原子自增操作
public final int getAndIncrement() {
for (;;) {
//获取value
int current = get();
int next = current + 1;
//value值没有变,说明其他线程没有自增过,将value设置为next
if (compareAndSet(current, next))
return current;
//否则说明value值已经改变,回到循环开始处,重新获取value。
}
}
由于锁的粒度是hash桶,多个put线程只有在请求同一个hash桶时,才会被阻塞。请求不同hash桶的put线程,可以并发执行。
put线程,请求的hash桶为空时,采用for循环+CAS的方式无锁插入。
remove方法
和put方法一样,多个remove线程请求不同的hash桶时,可以并发执行