数据结构:
在JDK1.7中
ConcurrentHashMap是由 Segment 数组、HashEntry 组成,和 HashMap 一样,仍然是数组加链表。
其中 HashEntry 用来封装具体的K/V对,是个典型的四元组;Segment 用来充当锁的角色,每个 Segment 对象守护整个ConcurrentHashMap的若干个桶 (可以把Segment看作是一个小型的哈希表),
其中每个桶是由若干个 HashEntry 对象链接起来的链表。总的来说,一个ConcurrentHashMap实例中包含由若干个Segment实例组成的数组,而一个Segment实例又包含由若干个桶,每个桶中都包含一条由若干个HashEntry对象链接起来的链表。
特别地,ConcurrentHashMap在默认并发级别下会创建16个Segment对象的数组,如果键能均匀散列,每个Segment大约守护整个散列表中桶总数的1/16。
在JDK1.8中
ConcurrentHashMap参考了JDK8 HashMap的实现,采用了数组+链表+红黑树的实现方式来设计,内部大量采用CAS操作。并发控制使synchronized和CAS来操作。
也将1.7中存放数据的HashEntry改为Node,但作用都是相同的。其中的 val next 都用了volatile修饰,保证了可见性。
属性字段:
// 用于定位段,大小等于segments数组的大小减 1,是不可变的
final int segmentMask;
// 用于定位段,大小等于32(hash值的位数)减去对segments的大小取以2为底的对数值,是不可变的
final int segmentShift;
// ConcurrentHashMap的底层结构是一个Segment数组
final Segment<K,V>[] segments;
Segment:Segment类继承于ReentrantLock类,从而使得Segment对象能充当锁的角色。
每个Segment对象用来守护它的成员对象table中包含的若干个桶。table是一个由HashEntry对象组成的链表数组,table数组的每一个数组成员就是一个桶。
ConcurrentHashMap允许多个修改(写)操作并发进行,其关键在于使用了锁分段技术,它使用了不同的锁来控制对哈希表的不同部分进行的修改(写),而ConcurrentHashMap内部使用段(Segment)来表示这些不同的部分。
Segment中的字段及含义:
// Segment中元素的数量,可见的
transient volatile int count;
//对count的大小造成影响的操作的次数(比如put或者remove操作)
transient int modCount;
// 阈值,段中元素的数量超过这个值就会对Segment进行扩容
transient int threshold;
// 链表数组
transient volatile HashEntry<K,V>[] table;
// 段的负载因子,其值等同于ConcurrentHashMap的负载因子
final float loadFactor;
基本元素:HashEntry
HashEntry用来封装具体的键值对,是个典型的四元组。与HashMap中的Entry类似,HashEntry也包括同样的四个域,分别是key、hash、value和next。不同的是,在HashEntry类中,key,hash和next域都被声明为final的,value域被volatile所修饰,因此HashEntry对象几乎是不可变的,这是ConcurrentHashmap读操作并不需要加锁的一个重要原因。
ConcurrentHashMap构造函数
1、ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel)
---该构造函数意在构造一个具有指定容量、指定负载因子和指定段数目/并发级别(若不是2的幂次方,则会调整为2的幂次方)的空ConcurrentHashMap
2、ConcurrentHashMap(int initialCapacity, float loadFactor)
---该构造函数意在构造一个具有指定容量、指定负载因子和默认并发级别(16)的空ConcurrentHashMap
3、ConcurrentHashMap(int initialCapacity)
---该构造函数意在构造一个具有指定容量、默认负载因子(0.75)和默认并发级别(16)的空ConcurrentHashMap
4、ConcurrentHashMap()
---该构造函数意在构造一个具有默认初始容量(16)、默认负载因子(0.75)和默认并发级别(16)的空ConcurrentHashMap
5、ConcurrentHashMap(Map<? extends K, ? extends V> m)
---该构造函数意在构造一个与指定 Map 具有相同映射的 ConcurrentHashMap,其初始容量不小于 16 (具体依赖于指定Map的大小),负载因子是 0.75,并发级别是 16, 是 Java Collection Framework 规范推荐提供的
以上构造函数中有三个重要参数:initialCapacity(初始容量)、loadFactor(负载因子)和concurrencyLevel(并发级别),这三个参数进行构造并初始化segments数组、段偏移量segmentShift、段掩码segmentMask和每个segment的
put方法:
jdk1.7中:
1、尝试自旋获取锁。
2、如果重试的次数达到了 MAX_SCAN_RETRIES 则改为阻塞锁获取,保证能获取成功。
3、将当前Segment中的table通过key的hashcode定位到HashEntry。
4、遍历该 HashEntry,如果不为空则判断传入的key和当前遍历的key是否相等,相等则覆盖旧的value。
5、不为空则需要新建一个HashEntry并加入到Segment中,同时会先判断是否需要扩容。
6、最后会解除在1中所获取当前Segment的锁。
jdk1.8中:
1、根据key计算出hashcode。
2、判断是否需要进行初始化。
3、f 即为当前key定位出的Node,如果为空表示当前位置可以写入数据,利用CAS尝试写入,失败则自旋保证成功。
4、如果当前位置的hashcode == MOVED == -1,则需要进行扩容。
5、如果都不满足,则利用synchronized锁写入数据。
6、如果数量大于TREEIFY_THRESHOLD则要转换为红黑树。
get方法:
jdk1.7中:
只需要将 Key 通过 Hash 之后定位到具体的 Segment ,再通过一次 Hash 定位到具体的元素上。由于HashEntry中的value属性是用volatile关键词修饰的,保证了内存可见性,所以每次获取时都是最新值。
ConcurrentHashMap的get方法是非常高效的,因为整个过程都不需要加锁。
jdk1.8中:
1、根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。
2、如果是红黑树那就按照树的方式获取值。
3、就不满足那就按照链表的方式遍历获取值。
rehash方法:
ConcurrentHashMap的重哈希实际上是对ConcurrentHashMap的某个段的重哈希,因此ConcurrentHashMap的每个段所包含的桶位自然也就不尽相同。
由于扩容是按照2的幂次方进行的,所以扩展前在同一个桶中的元素,现在要么还是在原来的序号的桶里,或者就是原来的序号再加上一个2的幂次方,就这两种选择。
如果元素的hashcode的第k+1位是0,那么元素在新桶的序号就是和原桶的序号是相等的;如果第k+1位的值是1,那么元素在新桶的序号就是原桶的序号加上2^k。
ConcurrentHashMap不同于HashMap,它既不允许key值为null,也不允许value值为null。
ConcurrentHashMap的扩容机制
1.7版本
1、1.7版本的ConcurrentHashMap是基于Segment分段实现的
2、每个Segment相对于一个小型的HashMap
3、每个Segment内部会进行扩容,和HashMap的扩容逻辑类似
4、先生成新的数组,然后转移元素到新数组中
5、扩容的判断也是每个Segment内部单独判断的,判断是否超过阈值
1.8版本
1、1.8版本的ConcurrentHashMap不再基于Segment实现
2、当某个线程进行put时,如果发现ConcurrentHashMap正在进行扩容那么该线程一起进行扩容
3、如果某个线程put时,发现没有正在进行扩容,则将key-value添加到ConcurrentHashMap中,然后判断是否超过阈值,超过了则进行扩容
4、ConcurrentHashMap是支持多个线程同时扩容的
5、扩容之前也先生成一个新的数组
6、在转移元素时,先将原数组分组,将每组分给不同的线程来进行元素的转移,每个线程负责一组或多组的元素转移工作




