目录
ConcurrentHashMap简介
ConcurrentHashMap是Java中的一个线程安全的哈希表实现,它采用了锁分段的思想提高并发度,适用于多线程环境下进行哈希表操作。相比于普通的HashMap,在多线程环境下,ConcurrentHashMap可以提高并发性能和吞吐量。
ConcurrentHashMap基于分段锁技术实现。具体来说,ConcurrentHashMap将哈希表分为多个段(Segment),每个段都是一个独立的哈希表,且对于每个段的操作都是线程安全的。当多个线程并发访问不同的段时,它们之间是互不干扰的,因此可以实现真正的同时读写操作,从而提高了并发性能。除此之外,ConcurrentHashMap还提供了一些线程安全的方法,如putIfAbsent()、replace()、remove()等,在多线程环境下使用这些方法可以保证操作的原子性。
在JDK1.6版本中,ConcurrentHashMap使用了segment继承了ReentrantLock充当锁的角色,为每一个segment提供了线程安全的保障;segment维护了哈希散列表的若干个桶,每个桶由HashEntry构成的链表。
而到了JDK1.8版本,ConcurrentHashMap底层数据结构改变为采用数组+链表+红黑树的数据形式,并且使用了synchronized以及CAS无锁操作来保证线程安全性。
总之,如果需要在多线程环境下进行哈希表操作,ConcurrentHashMap是一个很好的选择。
关键属性及类
ConcurrentHashMap的关键属性
1、table:装载Node的数组,作为ConcurrentHashMap的数据容器。它采用懒加载方式,在第一次插入数据时进行初始化操作,并且数组的大小总是2的幂次方。
2、nextTable:在扩容时使用的数组。平时为null,只有在扩容时才会被赋值为非null。
3、sizeCtl:用来控制table数组的大小。根据是否初始化和是否正在扩容有几种情况:
- 当值为负数时:如果为-1表示正在初始化,如果为-N则表示当前正有N-1个线程进行扩容操作;
- 当值为正数时:如果当前数组为null,则表示table在初始化过程中,sizeCtl表示需要新建数组的长度;
- 若已经初始化,表示当前数据容器(table数组)的可用容量,也可以理解为临界值(即插入节点数超过该临界值就需要扩容)。具体指的是数组的长度n乘以加载因子loadFactor;
- 当值为0时,即数组长度为默认初始值。
4、sun.misc.Unsafe U:在ConcurrentHashMap的实现中,通过大量使用U.compareAndSwapXXXX方法来修改ConcurrentHashMap的一些属性。这些方法利用了CAS算法来保证线程安全性。CAS(Compare and Swap)是一种乐观策略,假设每一次操作都不会产生冲突,只有在冲突发生时才进行重试。CAS操作依赖于现代处理器指令集,通过底层的CMPXCHG指令来实现。
CAS(Compare and Swap)操作的核心思想就是通过比较当前变量的实际值V与期望的旧值O是否相同来判断是否有其他线程对该变量进行了修改。如果当前变量的实际值V与期望的旧值O相同,说明该变量没有被其他线程修改过,此时可以安全地将新值N赋给该变量。CAS操作会尝试将新值N赋给变量,并返回操作是否成功的结果。操作成功意味着变量的值被成功更新。但如果当前变量的实际值V与期望的旧值O不相同,说明该变量已经被其他线程修改过,此时将新值N赋给该变量操作就是不安全的,可能会导致数据不一致。在这种情况下,需要进行重试,重新比较当前变量的实际值和期望的旧值,直到成功为止。
在ConcurrentHashMap中,CAS操作是通过sun.misc.Unsafe类提供的方法实现的,该类可以直接操控内存和线程底层操作,可以理解为java中的"指针"。该成员变量的获取是在静态代码块中,具体来说,在Unsafe类中有一个私有的静态成员变量theUnsafe,用于持有Unsafe实例。在类加载时,静态代码块会执行,并通过反射来获取Unsafe实例并赋值给theUnsafe变量。
java
private static final sun.misc.Unsafe theUnsafe;
static {
try {
// 使用反射获取Unsafe实例
java.lang.reflect.Field unsafeField = sun.misc.Unsafe.class.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
theUnsafe = (sun.misc.Unsafe) unsafeField.get(null);
} catch (Exception e) {
throw new RuntimeException("无法获取不安全实例", e);
}
}
通过这种方式,可以绕过了Unsafe类的限制,获取到了Unsafe实例,从而可以使用其中的方法进行底层的内存和线程操作。
需要注意的是,由于sun.misc.Unsafe是Java平台的内部API,不建议直接在生产代码中使用它,因为它的不稳定性和不可移植性。
**总结:**这些属性在ConcurrentHashMap的实现中起着重要的作用,确保了线程安全性和并发操作的正确性。
ConcurrentHashMap的关键内部类
1、Node类用于存储ConcurrentHashMap中的键值对,并具有next域链接到下一个节点。它实现了Map.Entry接口,包含hash、key、val以及next等属性。为了保证内存可见性,这些属性都使用volatile修饰。
java
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
Node(int hash, K key, V val, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.val = val;
this.next = next;
}
// 实现Map.Entry接口
public K getKey() {
return key;
}
public V getValue() {
return val;
}
public V setValue(V value) {
V oldValue = val;
val = value;
return oldValue;
}
}
2、TreeNode类继承自Node类,用于在ConcurrentHashMap的红黑树中存储节点。它包含了红黑树中的节点结构,如parent、left、right、prev和red等属性。
java
static final class TreeNode<K,V> extends Node<K,V> {
TreeNode<K,V> parent; // 红黑树连接
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // 删除时需要解除下一个引用
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
// ...
}
3、TreeBin类是一个中间类,用于包装多个TreeNode节点。实际上,ConcurrentHashMap的数组中存储的是TreeBin对象,而不是TreeNode对象。TreeBin包含了红黑树的根节点root,以及first、waiter和lockState等属性,用于管理并发访问。
java
static final class TreeBin<K,V> extends Node<K,V> {
TreeNode<K,V> root; // 红黑树的根节点
volatile TreeNode<K,V> first; // 链表的第一个节点
volatile Thread waiter; // 当前等待锁的线程
volatile int lockState; // 锁的状态
// 锁的状态常量
static final int WRITER = 1; // 写锁
static final int WAITER = 2; // 等待写锁
static final int READER = 4; // 读锁
// ...
}
4、ForwardingNode类是一个特殊节点,在扩容时会出现。它的key、value和hash属性都为null,并且具有一个指向新表格(nextTable)的引用。
java
static final class ForwardingNode<K,V> extends Node<K,V> {
final Node<K,V>[] nextTable; // 指向下一个表
ForwardingNode(Node<K,V>[] tab) {
super(MOVED, null, null, null);
this.nextTable = tab;
}
// ...
}
**总结:**这些内部类协同工作,使得ConcurrentHashMap能够实现高效并发的键值对存储和访问操作。
CAS关键操作
在ConcurrentHashMap中,CAS操作常用于以下三个方法:
1、tabAt方法用来获取table数组中指定索引位置的节点元素,其定义如下:
java
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
它使用了Java的Unsafe类的getObjectVolatile方法读取指定内存地址的值,实现了与get()方法类似的功能。需要注意的是,在ConcurrentHashMap中,Node类型的元素是作为数组的元素存储的,因此需要使用位运算来计算内存地址。
2、casTabAt方法用来利用CAS操作设置table数组中指定索引位置的节点元素,其定义如下:
java
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
Node<K,V> c, Node<K,V> v) {
return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
它使用了Java的Unsafe类的compareAndSwapObject方法执行CAS操作。该方法接受四个参数:操作目标数组,目标数组元素的偏移量,期望值和新值。如果目标数组中指定位置的元素与期望值相等,则将其替换为新值,并返回true表示操作成功;否则不做任何修改,并返回false表示操作失败。
3、setTabAt方法用来设置table数组中指定索引位置的节点元素,它与casTabAt方法不同的是,它不使用CAS操作,而是直接覆盖原有元素,其定义如下:
java
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}
它使用了Java的Unsafe类的putObjectVolatile方法写入指定内存地址的值,实现了与set()方法类似的功能。需要注意的是,由于该方法直接覆盖原有元素,因此可能会导致数据丢失,应谨慎使用。
重点方法
实例构造器方法
在使用 ConcurrentHashMap 时,可以通过调用不同的构造器方法来创建对象。ConcurrentHashMap 提供了如下几个常用的构造器方法:
- ConcurrentHashMap():无参构造器方法,创建一个初始容量为16的 ConcurrentHashMap 对象,这个对象在创建时,其 table 数组还未初始化,只有在第一次插入数据时才会初始化。
- ConcurrentHashMap(int initialCapacity):带初始容量参数的构造器方法,创建一个指定初始容量的 ConcurrentHashMap 对象。
- ConcurrentHashMap(Map<? extends K, ? extends V> m):带 Map 参数的构造器方法,创建一个包含给定 Map 中所有映射的 ConcurrentHashMap 对象。
- ConcurrentHashMap(int initialCapacity, float loadFactor):带初始容量和加载因子参数的构造器方法,创建一个指定初始容量和加载因子的 ConcurrentHashMap 对象。
- ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel):带初始容量、加载因子和并发级别参数的构造器方法,创建一个指定初始容量、加载因子和并发级别的 ConcurrentHashMap 对象。
需要注意的是,这些参数的具体含义如下:
- initialCapacity:初始容量,即 ConcurrentHashMap 在创建时的大小,默认为16。
- loadFactor:加载因子,用于计算 ConcurrentHashMap 容量的阈值,默认为0.75。
- concurrencyLevel:并发级别,表示可同时更新的线程数,默认为16。在并发修改时,使用分段锁机制来提高并发性能,将数据分成多个段,每个段都有自己的锁,不同的线程可以同时操作不同的段。
根据实际需求,选择合适的构造器方法创建 ConcurrentHashMap 对象,并传入相应的参数值即可。
java
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class main {
public static void main(String[] args) {
// 构造器方法1:无参构造器方法
ConcurrentHashMap<String, Integer> map1 = new ConcurrentHashMap<>();
// 构造器方法2:带初始容量参数的构造器方法
ConcurrentHashMap<String, Integer> map2 = new ConcurrentHashMap<>(16);
// 构造器方法3:带Map参数的构造器方法
ConcurrentHashMap<String, Integer> originalMap = new ConcurrentHashMap<>();
originalMap.put("key1", 1);
originalMap.put("key2", 2);
ConcurrentHashMap<String, Integer> map3 = new ConcurrentHashMap<>(originalMap);
// 构造器方法4:带初始容量、加载因子和并发级别参数的构造器方法
ConcurrentHashMap<String, Integer> map4 = new ConcurrentHashMap<>(16, 0.75f, 4);
// 构造器方法5:带初始容量参数和并发级别参数的构造器方法
ConcurrentHashMap<String, Integer> map5 = new ConcurrentHashMap<>(16, 4);
ExecutorService executorService = Executors.newFixedThreadPool(4);
// 创建五个线程,每个线程向不同的map中插入和移除元素
executorService.execute(() -> {
for (int i = 0; i < 10; i++) {
map1.put("Thread1:" + i, i);
map1.remove("Thread1:" + (i - 2));
}
});
executorService.execute(() -> {
for (int i = 0; i < 10; i++) {
map2.put("Thread2:" + i, i);
map2.remove("Thread2:" + (i - 2));
}
});
executorService.execute(() -> {
for (int i = 0; i < 10; i++) {
map3.put("Thread3:" + i, i);
map3.remove("Thread3:" + (i - 2));
}
});
executorService.execute(() -> {
for (int i = 0; i < 10; i++) {
map4.put("Thread4:" + i, i);
map4.remove("Thread4:" + (i - 2));
}
});
executorService.execute(() -> {
for (int i = 0; i < 10; i++) {
map5.put("Thread5:" + i, i);
map5.remove("Thread5:" + (i - 2));
}
});
executorService.shutdown();
while (!executorService.isTerminated()) {
// 等待所有线程执行完毕
}
// 输出各个map中的元素
System.out.println("map1: " + map1);
System.out.println("map2: " + map2);
System.out.println("map3: " + map3);
System.out.println("map4: " + map4);
System.out.println("map5: " + map5);
}
}
该示例代码创建了五个不同配置的 ConcurrentHashMap 对象,并使用五个线程并发地向每个 ConcurrentHashMap 中插入和移除元素。每个线程都执行类似的操作,以测试 ConcurrentHashMap 的并发安全性和效果。最后,输出每个 ConcurrentHashMap 中的元素。
initTable方法
initTable方法是在ConcurrentHashMap第一次插入数据时候调用,它会根据sizeCtl的值(即ConcurrentHashMap的大小)来初始化table数组,table数组是ConcurrentHashMap的主要存储数据结构。
java
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; // 定义table数组
int sc;
while ((tab = table) == null || tab.length == 0) { // 如果table为null或长度为0,则进行初始化操作
if ((sc = sizeCtl) < 0) { // 如果sizeCtl小于0,则让出当前线程的CPU时间片
Thread.yield();
} else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { // 如果成功获取到锁
try {
if ((tab = table) == null || tab.length == 0) { // 如果table仍然为null或长度为0,则进行初始化操作
int n = (sc > 0) ? sc : DEFAULT_CAPACITY; // 计算初始化长度
Node[] nt = new Node[n]; // 新建一个长度为n的Node数组
table = tab = nt; // 将新的table设置为当前的table
sc = n - (n >>> 2); // 更新sizeCtl
}
} finally {
sizeCtl = sc; // 释放锁并更新sizeCtl
}
break;
}
}
return tab; // 返回初始化后的table
}
这段代码实现的是ConcurrentHashMap的初始化方法,也就是当table为空或者长度为0时,初始化table。函数会返回初始化后的table。
put方法
put方法是向ConcurrentHashMap中插入一个键值对,它首先会对key进行hash计算得到一个桶的位置,在该桶的位置上加锁,然后查找当前桶中是否已经存在该key,如果存在则替换掉原有的value,否则将该键值对插入到桶中。如果桶的节点数超过了链表长度阈值(默认8),则会将该桶转换为红黑树,以提高查询性能。在插入完成后,如果节点数比sizeCtl的值大2倍,并且表还没有扩容,则会启动扩容操作。
java
public V put(K key, V value) {
if (value == null) { // value为空时抛出异常
throw new NullPointerException();
}
int hash = spread(key.hashCode()); // 计算hash值
int binCount = 0; // 记录链表或红黑树中节点个数
for (Node<K,V>[] tab = table;;) { // 循环查找table中是否包含key
Node<K,V> f; // 当前桶的第一个节点
int n, i, fh; // 当前table长度、当前桶的索引、当前桶的第一个节点的hash值
if (tab == null || (n = tab.length) == 0) { // 如果table为null或长度为0,则进行初始化操作
tab = initTable();
} else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { // 如果当前桶的第一个节点为null,则尝试在该桶中添加新节点
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null))) { // 使用CAS操作更新当前桶的第一个节点,如果成功,则添加成功
break;
}
} else if ((fh = f.hash) == MOVED) { // 如果当前桶的第一个节点的hash值为MOVED,表明当前桶正在进行扩容操作,需要帮助扩容
tab = helpTransfer(tab, f);
} else { // 如果当前桶的第一个节点既不为null也不为MOVED,则在链表或红黑树中查找key对应的节点
V oldVal = null; // 记录原来的值
synchronized (f) { // 对当前桶的第一个节点加锁
if (tabAt(tab, i) == f) { // 再次检查当前桶的第一个节点是否发生了变化,防止其它线程修改了该节点并将该节点向当前桶的后面移动
if (fh >= 0) { // 如果当前桶的第一个节点为链表节点
binCount = 1;
for (Node<K,V> e = f;; ++binCount) { // 遍历链表
K ek;
if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) { // 如果找到了key对应的节点
oldVal = e.value; // 记录原来的值
e.value = value; // 使用新值更新节点的value
break; // 替换成功
}
Node<K,V> pred = e;
if ((e = e.next) == null) { // 如果遍历到了链表的末尾,即没有找到key对应的节点
pred.next = new Node<K,V>(hash, key, value, null); // 在链表的末尾添加新节点
break; // 插入成功
}
}
} else if (f instanceof TreeBin) { // 如果当前桶的第一个节点为红黑树节点
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) { // 在红黑树中查找key对应的节点,并使用新值替换旧值
oldVal = p.value; // 记录原来的值
p.value = value;
} else {
++size; // 新增节点
}
break;
}
}
}
if (binCount != 0) { // 如果在链表或红黑树中找到了key对应的节点,则根据节点个数调整链表或红黑树的结构
if (binCount >= TREEIFY_THRESHOLD) { // 如果节点个数超过了阈值,需要将链表转化为红黑树
treeifyBin(tab, i);
}
if (oldVal != null) { // 找到了key对应的节点,需要返回旧值
return oldVal;
}
break;
}
}
}
addCount(1L, binCount); // 更新计数器并返回null
return null;
}
这段代码实现的是ConcurrentHashMap的put方法,用于向ConcurrentHashMap中插入键值对。如果该键已存在,会将原来的值替换为新值并返回原来的值;如果该键不存在,插入新的键值对并返回null。
get方法
get方法是从ConcurrentHashMap中根据key取值,它首先会对key进行hash计算得到一个桶的位置,然后查找当前桶中是否存在该key,如果不存在则返回null,否则返回该key对应的value。
java
public V get(Object key) {
Node<K,V>[] tab; // 当前的table
Node<K,V> e, p; // 用于遍历节点
int n, eh; // table长度,当前节点的hash值
K ek; // 当前节点的键
int h = spread(key.hashCode()); // 计算键的哈希值
if ((tab = table) != null && (n = tab.length) > 0 && (e = tabAt(tab, (n - 1) & h)) != null) { // 检查table是否为空,并且根据哈希值找到对应的桶
if ((eh = e.hash) == h && ((ek = e.key) == key || (ek != null && key.equals(ek)))) { // 如果第一个节点就是目标节点
return e.value; // 返回对应的值
} else if (eh < 0) { // 如果第一个节点的hash值小于0,说明该桶使用了红黑树存储节点
if ((p = e.find(h, key)) != null) { // 在红黑树中查找目标节点
return p.value; // 返回对应的值
}
} else { // 如果第一个节点不是目标节点,且不是红黑树节点,则在链表中遍历查找
while ((e = e.next) != null) {
if (e.hash == h && ((ek = e.key) == key || (ek != null && key.equals(ek)))) { // 如果找到了目标节点
return e.value; // 返回对应的值
}
}
}
}
return null; // 没有找到目标节点,返回null
}
这段代码实现的是ConcurrentHashMap的get方法,用于根据给定的键获取对应的值。代码中会根据键的哈希值计算出对应的桶索引,并在该桶中查找键对应的节点。如果找到了节点,则返回其对应的值;如果没有找到节点,则返回null。
transfer方法
transfer方法是在ConcurrentHashMap扩容时的转移数据操作,它会将源桶中的所有节点重新分配到新的桶中,同时保持节点之间的相对顺序不变。在节点重新分配过程中,如果桶中的节点数量小于等于6个,则会将其转换为链表,否则转换为红黑树。在节点分配完成后,如果节点数比sizeCtl的值小于等于当前ConcurrentHashMap大小的16分之一,并且表还没有收缩,则会启动收缩操作。
java
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride; // 当前table的长度和步长
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE) { // 根据CPU数量计算步长,如果小于最小步长,则使用最小步长
stride = MIN_TRANSFER_STRIDE;
}
if (nextTab == null) { // 如果nextTab为空,则创建一个容量为当前table两倍的新数组nextTab
try {
@SuppressWarnings("unchecked")
Node<K,V>[] nt = new Node[n << 1];
nextTab = nt;
} catch (Throwable ex) {
sizeCtl = Integer.MAX_VALUE; // 创建新数组失败时,设置sizeCtl为最大值,表示扩容失败
return;
}
nextTable = nextTab; // 将nextTab赋值给nextTable
transferIndex = n; // 将transferIndex设置为当前table的长度,表示从头开始迁移节点
}
int nextn = nextTab.length; // nextTab的长度
ForwardingNode<K,V> fwd = new ForwardingNode<>(nextTab); // 创建一个ForwardingNode节点,用于标记正在进行扩容的table
boolean advance = true; // 是否继续进行扩容操作
boolean finishing = false; // 是否是最后一次迁移操作
for (int i = 0, bound = 0;;) {
Node<K,V> f; // 当前节点
int fh; // 当前节点的哈希值
while (advance) {
int nextIndex, delta; // 下一个节点的索引和步长
while (i >= bound) { // 如果当前节点超过了bound,则更新bound,继续下一轮迁移
bound = (stride > 1) ? (bound + stride) : n;
if (bound <= n) { // bound小于等于n时,根据步长计算下一个i的值
i = bound - stride;
} else {
i = n;
advance = false; // 当i超过n时,停止迁移操作
break;
}
}
if (!advance) {
break;
}
if ((nextIndex = (delta = ((n + nextn) << 1) / n) * (i & (n - 1))) >= nextn || nextIndex < 0) {
throw new Error("Illegal forward index: " + nextIndex); // 检查计算得到的nextIndex是否越界
}
if (i + delta > bound) { // 如果计算得到的下一个bound超过了n,则将bound设置为n,并将delta调整为bound和i之间的距离
bound = n;
delta = bound - i;
}
int boundIndex = i + delta; // 计算boundIndex,表示下一轮迁移的边界
Object a;
while (i < boundIndex && (a = tabAt(tab, i)) == null) { // 在当前位置找到非空节点前的第一个节点
if (casTabAt(tab, i, null, fwd)) { // 将当前位置设置为fwd节点,标记为正在进行迁移操作
setTabAt(nextTab, nextIndex + (i & (n - 1)), a); // 在nextTab中设置对应位置的节点为a
break;
}
++i;
}
if (i >= boundIndex) { // 如果i超过了boundIndex,说明该范围内的节点都已经迁移完成,继续下一轮迁移
break;
}
if ((f = (Node<K,V>)a).hash < 0) { // 如果当前节点的哈希值小于0,说明已经被扩容过,直接跳过
if (f instanceof ForwardingNode) { // 如果当前节点是ForwardingNode节点,说明正在进行扩容
tab = ((ForwardingNode<K,V>)f).nextTable; // 获取下一个table,继续扩容
advance = true;
break;
}
continue; // 已经被扩容了,继续下一次循环
}
setTabAt(nextTab, nextIndex + (i & (n - 1)), f); // 将节点复制到nextTab中对应的位置
++i;
}
if (!advance) { // advance为false时,说明当前轮迁移完成,停止迁移操作
break;
}
finishing = casFinishing(false, true); // 将finishing状态CAS为true,表示最后一次迁移操作
}
if (finishing) { // 如果是最后一次迁移操作
recheckResize(); // 重新检查是否需要进行扩容
}
}
这段代码实现了ConcurrentHashMap的扩容操作。在并发环境下,当table中的节点数量达到一定阈值时,需要对table进行扩容,以提高并发性能。
在扩容过程中,首先计算扩容的步长(stride),然后创建一个新的数组nextTab作为扩容后的table。如果创建新数组失败,将sizeCtl设置为最大值,表示扩容失败。
接下来,通过循环迁移节点的方式,将原table中的节点复制到新的nextTab中。每次迁移的范围由步长控制,遍历当前范围内的节点,将其复制到nextTab的对应位置上。
在迁移过程中,会遇到一些特殊情况,如节点已经被扩容过、当前节点是ForwardingNode节点等,需要作出相应的处理。
最后,如果是最后一次迁移操作,将finishing状态设置为true,并重新检查是否需要进行扩容。
与size相关的一些方法
ConcurrentHashMap中的size()方法用于返回Map中的映射数量,即键值对的数量。由于ConcurrentHashMap可以包含更多的映射数,而无法以int表示,因此返回的是一个估计值。由于ConcurrentHashMap是一个并发安全的数据结构,多个线程可以同时进行插入和删除操作,因此在统计元素个数时,不可能在"stop the world"的情况下让其他线程都停下来。
为了解决这个问题,并发地统计元素数量并作出扩容决策,ConcurrentHashMap引入了mappingCount字段和addCount()方法。
1、mappingCount:它是一个用来统计元素数量的字段,并不一定保证准确性,但会尽可能反映出当前元素的数量。该方法与size()相似,但返回的是一个long类型的值。
java
//size()方法
public int size() {
long n = sumCount();
return ((n < 0L) ? 0 :
(n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
(int)n);
}
//上述代码实现了size()方法,它首先调用sumCount()方法获取映射数量,并将得到的结果转换为int类型返//回。具体转换逻辑如下:
//1、如果n小于0,则返回0。
//2、如果n大于Integer.MAX_VALUE,则返回Integer.MAX_VALUE。
//3、否则,将n转换为int类型返回。
//需要注意的是,由于存在并发插入或删除操作,所以返回的值是一个估计值,并不一定完全准确。
//mappingCount()方法
public long mappingCount() {
long n = sumCount();
return (n < 0L) ? 0L : n; // 忽略临时的负值
}
//另外,ConcurrentHashMap还提供了mappingCount()方法来返回映射数量。该方法与size()相似,但返回的是一个long类型的值。
//mappingCount()方法内部同样调用了sumCount()方法获取映射数量,然后直接返回该值。如果n小于0,则返回0。这里忽略了临时的负值,因为在计算映射数量时可能存在并发修改的情况。
2、addCount()方法:这个方法用于更新baseCount的值,并检查是否需要进行扩容。baseCount是通过累加mappingCount得到的一个近似值,用于快速估算元素数量。当调用addCount()方法时,会对baseCount进行更新,同时检查是否达到扩容的条件。
java
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
// 尝试使用CAS方法更新baseCount的值
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true;
// 如果counterCells为null或者长度小于0,或者无法获取到CounterCell,
// 或者无法使用CAS方法更新CounterCell的值,则调用fullAddCount()方法完成添加计数值的操作。
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
s = sumCount();
}
// 当check值大于等于0时,表示需要检查是否需要进行扩容操作。
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n);
//
if (sc < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
// 如果已经有其他线程在执行扩容操作,
// 则等待其他线程完成扩容操作;否则,当前线程尝试发起扩容操作。
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
// 当前线程是唯一的或是第一个发起扩容的线程,此时nextTable=null。
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
}
这是ConcurrentHashMap中的addCount()方法的源码。它用于将计数值添加到Map的大小中,并在需要时触发扩容操作。
在该方法中,首先尝试使用CAS方法更新baseCount的值。如果成功,直接返回。否则,尝试通过CounterCell来更新计数值。如果还是失败了,就调用fullAddCount()方法来完成添加计数值的操作。
当check参数大于等于0时,表示需要检查是否需要进行扩容操作。如果Map的大小已经超过了sizeCtl的值(初始值为DEFAULT_CAPACITY),并且表格还没有达到最大容量(MAXIMUM_CAPACITY),就会尝试进行扩容。在实现中,会判断当前是否有其他线程正在执行扩容操作。如果是,则先等待其他线程完成扩容操作;否则,当前线程就会尝试发起扩容操作。如果当前线程是第一个发起扩容操作的线程,则会将sizeCtl的值设为新的扩容戳记和2(表示当前线程正在扩容)的组合;如果已经有其他线程正在进行扩容操作,则会将sizeCtl的值加1,表示等待其他线程完成扩容操作。
需要注意的是,addCount()方法是ConcurrentHashMap实现中的内部方法,不建议直接修改或调用该方法。通常情况下,我们只需要使用ConcurrentHashMap提供的公共方法来操作Map,而不需要关心其内部实现。
注意:ConcurrentHashMap类的addCount()方法只在Java 8及以后的版本中支持。如果你使用的是较旧的Java版本,可以使用putIfAbsent()方法实现相同的功能。
java
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.LongAdder;
public class main {
public static void main(String[] args) {
// 创建一个线程安全的ConcurrentHashMap对象
ConcurrentHashMap<String, LongAdder> map = new ConcurrentHashMap<>();
// 添加一些数据到Map中
map.put("Apple", new LongAdder());
map.put("Banana", new LongAdder());
map.put("Orange", new LongAdder());
// 输出初始的size值
System.out.println("初始的size值:" + map.size());
// 使用putIfAbsent()方法将新的键值对添加到Map中,并更新size的值
String key = "Grapes";
LongAdder value = new LongAdder();
value.increment(); // 假设value需要自增1
map.putIfAbsent(key, value);
// 输出添加后的size值
System.out.println("添加后的size值:" + map.size());
// 输出添加后的键值对
LongAdder result = map.get(key);
if (result != null) {
System.out.println("添加后的键值对:" + result.intValue());
} else {
System.out.println("键值对不存在");
}
}
}
总之,ConcurrentHashMap通过mappingCount和addCount()方法来估计元素的数量,并在适当的时机进行扩容。这种设计可以在一定程度上提高并发性能,但也导致了size()方法返回的结果可能不是完全准确的。如果需要获取准确的元素个数,可以考虑使用加锁等方式来保证操作的原子性。
总结
总结起来,JDK6和JDK7中的ConcurrentHashMap使用了Segment来减小锁粒度,并且在put操作时需要锁住Segment,而get操作不需要加锁。它使用volatile关键字来确保可见性,并且在统计全局映射数量时会多次尝试计算modcount来判断是否有其他线程进行了修改操作,如果没有,则直接返回size。如果有,则需要依次锁住所有的Segment来计算。
而JDK8之后的ConcurrentHashMap摒弃了Segment的设计,直接针对Node数组中的每一个桶进行处理,减小了锁粒度。为了防止链表过长导致性能下降,当链表长度超过8时,会转换为红黑树。
主要的设计变化包括:
- 不再使用Segment,而是使用Node来减小锁粒度。
- 引入MOVED状态,在resize过程中,其他线程仍然可以进行put操作而不会被阻塞。
- 使用3个CAS操作来保证Node的一些操作的原子性,取代了锁的使用。
- sizeCtl的不同取值代表不同的含义,起到控制作用。
- 使用synchronized关键字而不是ReentrantLock。
这些改进使得新版的ConcurrentHashMap在并发场景下具有更好的性能和扩展性。
注意:更多关于1.7版本与1.8版本的ConcurrentHashMap的实现对比,可以参考这篇文章。