推荐阅读:https://www.jianshu.com/p/e694f1e868ec
0.特点
ConcurrentHashMap在java.util.Concurrent包下,最大的特点是安全、性能较同样安全的HashTable更高。
它的设计与实现非常精巧,大量的利用了volatile,final,CAS等lock-free技术来减少锁竞争对于性能的影响。
ConcurrentHashMap中的HashEntry相对于HashMap中的Entry有一定的差异性:HashEntry中的value以及next都被volatile修饰,这样在多线程读写过程中能够保持它们的可见性。
1.结构
JDK7:Segment(ReentrantLock可重入锁)+Entry、链表(Hash表)
JDK8:CAS+Synchronized+数组、链表、红黑树(Node)
结构图
JDK1.7:
JDK1.8:
其实可以看出JDK1.8版本的ConcurrentHashMap的数据结构已经接近HashMap。
1.数据结构更精巧:取消了Segment分段锁的数据结构,取而代之的是数组+链表+红黑树的结构。
2.锁的方式更简单:JDK1.7采用segment的分段锁机制实现线程安全,其中segment继承自ReentrantLock。JDK1.8采用CAS+Synchronized保证线程安全。
3.锁的粒度变细:原来是对需要进行数据操作的Segment加锁,现调整为对每个数组元素加锁(Node)。
4.链表转化为红黑树:定位结点的hash算法简化会带来弊端,Hash冲突加剧,因此在链表节点数量大于8时,会将链表转化为红黑树进行存储。
5.查询时间复杂度变小:从原来的遍历链表O(n),变成遍历红黑树O(logN)。
2.初始化
JDK7
Segment数组的大小默认为16,Segment中的HashEntry数组大小最小值为2。它们大小都要为2的幂次方。
JDK8
懒加载策略,和HashMap有相似。
在第一次put的时候才会初始化,Node数组默认大小为16
下面是源代码中的一些默认参数,可以看出和HashMap很相似了。
private static final int DEFAULT_CAPACITY = 16;
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private static final int MAXIMUM_CAPACITY = 1 << 30;
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
private static final float LOAD_FACTOR = 0.75f;
static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;
static final int MOVED = -1; // hash for forwarding nodes
static final int TREEBIN = -2; // hash for roots of trees
static final int RESERVED = -3; // hash for transient reservations
static final int HASH_BITS = 0x7fffffff;
3.添加元素
JDK7
1.计算Segment数组的相应位置,如果还没有初始化,那就CAS赋值,执行Segment的put插入数据。
2.如果执行Segment对象时,已经有线程获取锁,那么该线程就重复执行tryLock()获取锁,最多重复64次,超过次数就挂起该线程。
每个线程执行插入操作之前要获取锁,执行完要释放锁,并且唤醒等待该Segment对象的其他线程。
JDK8
1.如果该位置的Node还未被初始化,那就CAS插入相关数据。
2.如果该位置已经初始化,那就加Synchronized锁,插入(判断结构是链表还是红黑树,按对应方式插入)
3.更新个数,检查是否需要树化。
4.计算个数
JDK7
因为总个数=每个Segment中的个数相加,但是这是不断变化的,所以采用如下方法:
1.先不加锁,连续计算3次总的个数。
2.如果前后两次结果计算相同,那么这就是准确的,返回即可。
3.如果都不同,则给每个Segment进行加锁,再计算个数。
JDK8
使用一个volatile修饰的变量记录元素个数,当插入/删除数据时,会更新个数。
元素个数保存在baseCount中,部分元素个数变化保存在CountCell中,通过累加CountCell和baseCount的数量即可算出总个数。
1.并发很高时,使用CountCell记录元素个数的变化。
2.如果CountCell数组的CountCells为空,那就初始化,并CAS插入对应数据。
3.如果插入失败,尝试CAS修改baseCount字段,若成功就退出,不成功就继续CAS插入CountCell。