但是又为何需要学习ConcurrentHashMap?用不就完事了?我认为学习其源码有两个好处:
- 更灵活的运用ConcurrentHashMap
- 欣赏并发编程大师Doug Lea的作品,源码中有很多值得我们学习的并发思想,要意识到,线程安全不仅仅只是加锁
ConcurrentHashMap是怎么做到线程安全的?
get方法如何线程安全地获取key、value?
put方法如何线程安全地设置key、value?
size方法如果线程安全地获取容器容量?
底层数据结构扩容时如果保证线程安全?
初始化数据结构时如果保证线程安全?
ConcurrentHashMap并发效率是如何提高的?
和加锁相比较,为什么它比HashTable效率高?
参考链接:https://blog.csdn.net/qq_41737716/article/details/90549847
程序中的可伸缩性(提升外部资源即可提升并发性能的比率)是由程序中串行执行部分所影响的,而常见的串行执行有锁竞争(上下文切换消耗、等待、串行)等等,这给了我们一个启发,可以通过减少锁竞争来优化并发性能,而ConcurrentHashMap则使用了锁分段(减小锁范围)、CAS(乐观锁,减小上下文切换开销,无阻塞)等等技
相关概念
Amdahl定律
此节定律描述均来自《Java并发编程实战》一书
假设F是必须被串行执行的部分,N代表处理器数量,Speedup代表加速比,可以简单理解为CPU使用率
N越大,处理器越多,串行执行越少,cpu利用率越大
此公式告诉我们,当N趋近无限大,加速比最大趋近于1/F;
初始化数据结构时的线程安全
HashMap的底层数据结构这里简单带过一下,不做过多赘述:
在这里插入图片描述
大致是以一个Node对象数组来存放数据,Hash冲突时会形成Node链表,在链表长度超过8,Node数组超过64时会将链表结构转换为红黑树,Node对象:
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
...
}
总结
就算有多个线程同时进行put操作,在初始化数组时使用了乐观锁CAS操作来决定到底是哪个线程有资格进行初始化,其他线程均只能等待。
用到的并发技巧:
volatile变量(sizeCtl):它是一个标记位,用来告诉其他线程这个坑位有没有人在,其线程间的可见性由volatile保证。
CAS操作:CAS操作保证了设置sizeCtl标记位的原子性,保证了只有一个线程能设置成功由于其减小了锁的粒度,若Hash完美不冲突的情况下,可同时支持n个线程同时put操作,n为Node数组大小,在默认大小16下,可以支持最大同时16个线程无竞争同时操作且线程安全。当hash冲突严重时,Node链表越来越长,将导致严重的锁竞争,此时会进行扩容,将Node进行再散列,下面会介绍扩容的线程安全性。总结一下用到的并发技巧:
减小锁粒度:将Node链表的头节点作为锁,若在默认大小16情况下,将有16把锁,大大减小了锁竞争(上下文切换),就像开头所说,将串行的部分最大化缩小,在理想情况下线程的put操作都为并行操作。同时直接锁住头节点,保证了线程安全
Unsafe的getObjectVolatile方法:此方法确保获取到的值为最新扩容操作的线程安全
在扩容时,ConcurrentHashMap支持多线程并发扩容,在扩容过程中同时支持get查数据,若有线程put数据,还会帮助一起扩容,这种无阻塞算法,将并行最大化的设计,堪称一绝。
先来看看扩容代码实现:
ConcurrentHashMap运用各类CAS操作,将扩容操作的并发性能实现最大化,在扩容过程中,就算有线程调用get查询方法,也可以安全的查询数据,若有线程进行put操作,还会协助扩容,利用sizeCtl标记位和各种volatile变量进行CAS操作达到多线程之间的通信、协助,在迁移过程中只锁一个Node节点,即保证了线程安全,又提高了并发性能。
参考链接:https://blog.csdn.net/qq_41737716/article/details/90549847