参考:https://www.bilibili.com/video/BV1yT411H7YK
一、关于ConcurrentHashMap
ConcurrentHashMap是Java中的一个线程安全的哈希表实现,它是对HashMap的并发扩展。它提供了高效的并发读写操作,使得多个线程可以同时访问和修改哈希表,而无需显式地进行同步。
ConcurrentHashMap 是一种线程安全的高效Map集合,底层数据结构:
-
JDK1.7底层采用分段的数组+链表实现
-
JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。
JDK 1.7
数据结构
- 提供了一个segment数组,在初始化ConcurrentHashMap 的时候可以指定数组的长度,默认是16,一旦初始化之后中间不可扩容
- 在每个segment中都可以挂一个HashEntry数组,数组里面可以存储具体的元素,HashEntry数组是可以扩容的
- 在HashEntry存储的数组中存储的元素,如果发生冲突,则可以挂单向链表
存储流程
- 先去计算key的hash值,然后确定segment数组下标
- 再通过hash值确定hashEntry数组中的下标存储数据
- 在进行操作数据的之前,会先判断当前segment对应下标位置是否有线程进行操作,为了线程安全使用的是ReentrantLock进行加锁,如果获取锁失败会使用cas自旋锁进行尝试
JDK1.8
在JDK1.8中,放弃了Segment臃肿的设计,数据结构跟HashMap的数据结构是一样的:数组+红黑树+链表
采用 CAS + Synchronized来保证并发安全进行实现
-
CAS控制数组节点的添加
-
synchronized只锁定当前链表或红黑二叉树的首节点,只要hash不冲突,就不会产生并发的问题 , 效率得到提升
二、类成员变量
非常抱歉,让我用中文再解释一遍。
以下是JDK 1.8中ConcurrentHashMap源码中的类成员变量,并附有说明:
public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>
implements ConcurrentMap<K, V>, Serializable {
// 默认初始容量
static final int DEFAULT_INITIAL_CAPACITY = 16;
// 默认装载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 默认并发级别
static final int DEFAULT_CONCURRENCY_LEVEL = 16;
// 最大容量,必须是2的幂次方
static final int MAXIMUM_CAPACITY = 1 << 30;
// 链表转换为红黑树的阈值
static final int TREEIFY_THRESHOLD = 8;
// 红黑树转换为链表的阈值
static final int UNTREEIFY_THRESHOLD = 6;
// 不进行树化的最小容量
static final int MIN_TREEIFY_CAPACITY = 64;
// 哈希桶数组,存储键值对
transient volatile Node<K,V>[] table;
// 下一个扩容阶段的哈希桶数组
private transient volatile Node<K,V>[] nextTable;
// 基本计数器,用于统计键值对的数量
private transient volatile long baseCount;
// 控制变量,用于控制并发扩容和其他操作
private transient volatile int sizeCtl;
}
这些变量的作用:
-
DEFAULT_INITIAL_CAPACITY
:默认初始容量
这个变量定义了ConcurrentHashMap的默认初始容量,即在没有指定初始容量的情况下使用的容量大小。默认值为16。 -
DEFAULT_LOAD_FACTOR
:默认装载因子
装载因子是指哈希表在达到多满时进行扩容的阈值比例。这个变量定义了ConcurrentHashMap的默认装载因子,即在没有指定装载因子的情况下使用的比例值。默认值为0.75。 -
DEFAULT_CONCURRENCY_LEVEL
:默认并发级别
并发级别是指可以同时更新的线程数。这个变量定义了ConcurrentHashMap的默认并发级别,即在没有指定并发级别的情况下使用的级别。默认值为16。 -
MAXIMUM_CAPACITY
:最大容量
这个变量定义了ConcurrentHashMap哈希表的最大容量。由于哈希表的容量必须是2的幂次方,所以最大容量为2^30。 -
TREEIFY_THRESHOLD
:链表转换为红黑树的阈值
当哈希桶中的链表长度达到这个阈值时,链表将被转换为红黑树,以提高查找性能。默认值为8。 -
UNTREEIFY_THRESHOLD
:红黑树转换为链表的阈值
当红黑树中的节点数量小于这个阈值时,红黑树将被转换回链表结构。默认值为6。 -
MIN_TREEIFY_CAPACITY
:不进行树化的最小容量
当哈希桶的容量小于这个值时,不会进行树化操作,即不会将链表转换为红黑树。默认值为64。 -
table
:哈希桶数组,存储键值对
这个变量是ConcurrentHashMap的核心数据结构,用于存储键值对。它是一个哈希桶数组,每个桶存储一个链表或红黑树结构。 -
nextTable
:下一个扩容阶段的哈希桶数组
在哈希表进行扩容时,会创建一个新的哈希桶数组,nextTable
变量用于指向这个新的数组。 -
baseCount
:基本计数器,用于统计键值对的数量
这个变量用于记录ConcurrentHashMap中存储的键值对的数量,它是一个基本计数器。 -
sizeCtl
:控制变量,用于控制并发扩容和其他操作
这个变量用于控制并发扩容和其他操作,它的值会根据不同的情况进行相应的调整。
这些变量在ConcurrentHashMap的实现中起着关键的作用,控制着容量、并发级别、扩容机制以及存储键值对的数据结构等方面。
三、构造方法
JDK 1.8中ConcurrentHashMap源码中的构造方法:
- 默认构造方法:
public ConcurrentHashMap() {
// 使用默认初始容量、默认装载因子和默认并发级别创建ConcurrentHashMap
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
}
这个构造方法使用默认的初始容量(16)、默认的装载因子(0.75)和默认的并发级别(16)来创建ConcurrentHashMap。
- 指定初始容量的构造方法:
public ConcurrentHashMap(int initialCapacity) {
// 使用指定初始容量、默认装载因子和默认并发级别创建ConcurrentHashMap
this(initialCapacity, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
}
这个构造方法使用指定的初始容量来创建ConcurrentHashMap,同时使用默认的装载因子(0.75)和默认的并发级别(16)。
- 指定初始容量和装载因子的构造方法:
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
// 使用指定初始容量、指定装载因子和默认并发级别创建ConcurrentHashMap
this(initialCapacity, loadFactor, DEFAULT_CONCURRENCY_LEVEL);
}
这个构造方法使用指定的初始容量和装载因子来创建ConcurrentHashMap,同时使用默认的并发级别(16)。
- 指定初始容量、装载因子和并发级别的构造方法:
public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) {
// 根据指定的初始容量、装载因子和并发级别来创建ConcurrentHashMap
if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0) {
throw new IllegalArgumentException();
}
// 省略了一些初始化逻辑...
}
这个构造方法使用指定的初始容量、装载因子和并发级别来创建ConcurrentHashMap。在创建之前,会进行一些参数的检查,确保装载因子大于0,初始容量非负,以及并发级别大于0。
这些构造方法提供了不同的选项来创建ConcurrentHashMap,可以根据需求选择适合的构造方法。
四、添加元素的方法
ConcurrentHashMap
中添加元素的方法有put
方法和putAll
方法:
public V put(K key, V value) {
return putVal(key, value, false);
}
/**
* Implementation for put and putIfAbsent
*/
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null)
throw new NullPointerException(); // 检查键和值是否为null,如果是,则抛出NullPointerException异常
int hash = spread(key.hashCode()); // 计算键的哈希值
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable(); // 初始化桶数组
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
break; // 当向空桶添加元素时,不需要加锁
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f); // 如果桶正在进行扩容,则帮助扩容操作
else {
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)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
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) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i); // 如果链表长度超过阈值,将链表转换为红黑树
if (oldVal != null)
return oldVal; // 返回旧值
break;
}
}
}
addCount(1L, binCount); // 更新计数器
return null;
}
/**
* Copies all of the mappings from the specified map to this one.
* These mappings replace any mappings that this map had for any of the
* keys currently in the specified map.
*
* @param m mappings to be stored in this map
*/
public void putAll(Map<? extends K, ? extends V> m) {
tryPresize(m.size()); // 预调整桶数组的大小
for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
putVal(e.getKey(), e.getValue(), false); // 逐个添加映射到当前map中
}
这些方法的主要步骤包括:
put
方法:- 参数检查:检查键和值是否为null,如果是,则抛出
NullPointerException
异常。 - 计算哈希值:根据键的哈希码计算出哈希值。
- 遍历桶数组:根据哈希值和桶数组的长度确定要放置元素的桶位置。
- 桶为空:如果桶为空,则通过CAS操作尝试将新节点放置到桶中。
- 桶正在扩容:如果桶正在进行扩容操作,则帮助进行扩容。
- 遍历这段代码是
ConcurrentHashMap
类中的put
方法和putAll
方法的实现。
- 参数检查:检查键和值是否为null,如果是,则抛出
public V put(K key, V value) {
return putVal(key, value, false);
}
这个方法用于将键值对放入ConcurrentHashMap
中。它调用了putVal
方法,将onlyIfAbsent
参数设置为false
,表示不仅仅在键不存在时才添加。
接下来,让我们看一下putVal
方法的实现:
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null)
throw new NullPointerException(); // 检查键和值是否为null,如果是,则抛出NullPointerException异常
int hash = spread(key.hashCode()); // 计算键的哈希值
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
// ...
}
addCount(1L, binCount); // 更新计数器
return null;
}
这个方法是put
方法的实际实现:
- 它首先检查键和值是否为
null
,如果是,则抛出NullPointerException
异常。 - 然后,它使用
spread
方法计算键的哈希值。 - 接下来,它使用一个无限循环来遍历桶数组并找到要放置元素的位置。在循环中,它会根据不同的情况执行不同的操作,包括:
- 如果桶为空,则使用CAS操作将新节点放入桶中。
- 如果桶正在进行扩容操作,则帮助进行扩容。
- 如果桶中已经存在节点,则根据节点类型执行不同的操作:如果是链表节点,则遍历链表找到对应的节点并更新值;如果是红黑树节点,则在树中插入或更新节点。
- 最后,它会更新计数器并返回
null
。
接下来,让我们来看一下putAll
方法:
public void putAll(Map<? extends K, ? extends V> m) {
tryPresize(m.size()); // 预调整桶数组的大小
for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
putVal(e.getKey(), e.getValue(), false); // 逐个添加映射到当前map中
}
这个方法用于将另一个Map
中的所有映射添加到当前的ConcurrentHashMap
中。它首先调用tryPresize
方法来预调整桶数组的大小,然后使用循环逐个将映射添加到当前的ConcurrentHashMap
中。
五、删除元素
当涉及到HashMap的remove
操作时,这段代码实现了remove
方法。remove
方法通过调用replaceNode
方法来实现节点的替换和删除。
public V remove(Object key) {
return replaceNode(key, null, null);
}
/**
* 实现四个公共的remove/replace方法:
* 根据cv的匹配情况,将节点的值替换为v。如果结果值为null,则删除节点。
*/
final V replaceNode(Object key, V value, Object cv) {
int hash = spread(key.hashCode());
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// 检查tab是否为null或长度为0,或者tab[i]为null
if (tab == null || (n = tab.length) == 0 ||
(f = tabAt(tab, i = (n - 1) & hash)) == null)
break;
// 如果tab[i]的hash值为MOVED,则帮助进行迁移操作
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
boolean validated = false;
// 对节点f进行同步操作
synchronized (f) {
// 再次检查tab[i]是否为f
if (tabAt(tab, i) == f) {
// 如果fh >= 0,表示f是普通节点
if (fh >= 0) {
validated = true;
// 遍历链表或红黑树
for (Node<K,V> e = f, pred = null;;) {
K ek;
// 判断节点e的hash值和key是否相等
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
V ev = e.val;
// 如果cv为null,或cv等于ev,或cv与ev相等,则执行替换或删除操作
if (cv == null || cv == ev ||
(ev != null && cv.equals(ev))) {
oldVal = ev;
if (value != null)
e.val = value;
else if (pred != null)
pred.next = e.next;
else
setTabAt(tab, i, e.next);
}
break;
}
pred = e;
if ((e = e.next) == null)
break;
}
}
// 如果f是TreeBin类型,则表示f是红黑树
else if (f instanceof TreeBin) {
validated = true;
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> r, p;
// 在红黑树中查找指定的节点
if ((r = t.root) != null &&
(p = r.findTreeNode(hash, key, null)) != null) {
V pv = p.val;
// 如果cv为null,或cv等于pv,或cv与pv相等,则执行替换或删除操作
if (cv == null || cv == pv ||
(pv != null && cv.equals(pv))) {
oldVal = pv;
if (value != null)
p.val = value;
else if (t.removeTreeNode(p))
setTabAt(tab, i, untreeify(t.first));
}
}
}
}
}
if (validated) {
// 如果成功替换或删除节点的值,则根据value的情况更新计数器并返回旧的值
if (oldVal != null) {
if (value == null)
addCount(-1L, -1);
return oldVal;
}
break;
}
}
}
return null;
}
当调用remove
方法时,它会调用replaceNode
方法来执行删除操作。下面是replaceNode
方法的逻辑解释:
- 首先,根据传入的键
key
计算出哈希值hash
。 - 进入一个无限循环,遍历哈希表中与
hash
对应的位置上的节点。 - 如果当前位置上的节点
f
为null
,则跳出循环。 - 如果当前位置上的节点的
hash
值为MOVED
,表示正在进行扩容操作,需要帮助进行迁移操作。 - 如果当前位置上的节点是普通节点(非红黑树),则进行以下操作:
- 遍历节点链表或红黑树,查找与给定的键
key
相等的节点。 - 如果找到匹配的节点
e
,并且满足条件:cv
为null
、cv
与节点的值ev
相等、或者cv
与ev
相等,则执行替换或删除操作。 - 如果值被替换或删除,则更新旧值
oldVal
,如果当前操作是删除操作,则更新计数器。 - 跳出循环。
- 遍历节点链表或红黑树,查找与给定的键
- 如果当前位置上的节点是红黑树,表示当前节点是
TreeBin
类型,则进行以下操作:- 在红黑树中查找与给定的键
key
相等的节点。 - 如果找到匹配的节点
p
,并且满足条件:cv
为null
、cv
与节点的值pv
相等、或者cv
与pv
相等,则执行替换或删除操作。 - 如果值被替换或删除,则更新旧值
oldVal
,如果当前操作是删除操作,则更新计数器。 - 跳出循环。
- 在红黑树中查找与给定的键
- 如果成功替换或删除了节点的值,则根据
value
的情况更新计数器,并返回旧的值。 - 如果没有找到匹配的节点,则继续循环遍历哈希表中的下一个位置。
- 如果遍历完整个哈希表都没有找到匹配的节点,则返回
null
表示没有进行删除操作。
通过遍历哈希表中的节点链表或红黑树,找到与给定键相等的节点,并根据条件执行替换或删除操作。它使用了同步来确保在并发情况下的一致性,并根据操作的结果更新计数器并返回旧的值。
clear()
方法,用于清空哈希表。它通过遍历哈希表中的节点数组,并对每个位置上的节点进行删除操作。
public void clear() {
long delta = 0L; // 负数的删除计数
int i = 0;
Node<K,V>[] tab = table;
// 遍历哈希表中的节点数组
while (tab != null && i < tab.length) {
int fh;
Node<K,V> f = tabAt(tab, i);
// 如果节点为null,说明当前位置无节点,继续下一个位置
if (f == null)
++i;
// 如果节点的hash值为MOVED,表示正在进行扩容操作,需要帮助进行迁移操作
else if ((fh = f.hash) == MOVED) {
tab = helpTransfer(tab, f);
i = 0; // 重新开始
}
else {
// 对节点进行同步操作
synchronized (f) {
// 再次检查tab[i]是否为f
if (tabAt(tab, i) == f) {
Node<K,V> p = (fh >= 0 ? f :
(f instanceof TreeBin) ?
((TreeBin<K,V>)f).first : null);
// 遍历链表或红黑树,对每个节点进行删除计数,并将节点置为null
while (p != null) {
--delta;
p = p.next;
}
setTabAt(tab, i++, null);
}
}
}
}
// 如果删除计数不为0,则根据计数更新计数器
if (delta != 0L)
addCount(delta, -1);
}
clear
方法的逻辑如下:
- 初始化一个变量
delta
,用于记录删除操作的计数,初始值为0。 - 初始化变量
i
为0,表示当前遍历的节点数组的索引。 - 获取哈希表中的节点数组
tab
。 - 进入一个循环,条件是节点数组
tab
不为null且当前索引i
小于节点数组的长度。 - 在循环中,首先获取节点数组
tab
中索引为i
的节点f
。 - 如果节点
f
为null,说明当前位置无节点,将索引i
加1,继续下一个位置。 - 如果节点
f
的哈希值fh
等于MOVED
,表示当前节点正在进行扩容操作,需要帮助进行迁移操作。- 调用
helpTransfer
方法,将节点数组tab
和当前节点f
作为参数,执行帮助迁移操作。 - 将索引
i
重新设置为0,重新开始遍历节点数组。
- 调用
- 如果节点
f
不为null且哈希值fh
不等于MOVED
,表示当前节点为普通节点。- 获取当前节点
f
的同步锁。 - 再次检查节点数组
tab
中索引为i
的节点是否为f
,避免在获取锁之前节点发生了变化。 - 如果节点数组
tab
中索引为i
的节点仍然为f
,则执行以下操作:- 根据节点的哈希值判断节点类型:
- 如果节点的哈希值
fh
大于等于0,表示当前节点为普通节点。- 将节点
f
赋值给变量p
。
- 将节点
- 如果节点是
TreeBin
类型的节点,表示当前节点为红黑树节点。- 将红黑树节点
f
的first
节点赋值给变量p
,即红黑树的第一个节点。
- 将红黑树节点
- 如果节点的哈希值
- 遍历链表或红黑树,对每个节点进行删除操作:
- 每遍历到一个节点,将删除计数
delta
减1。 - 将当前节点的下一个节点赋值给变量
p
,继续遍历下一个节点。
- 每遍历到一个节点,将删除计数
- 将节点数组
tab
中索引为i
的位置置为null,表示删除节点。
- 根据节点的哈希值判断节点类型:
- 获取当前节点
- 结束同步块。
- 将索引
i
加1,继续下一个位置的遍历。 - 如果删除计数
delta
不等于0,表示执行了删除操作:- 调用
addCount
方法,将删除计数delta
和-1作为参数,根据计数更新计数器。
- 调用
- 完成循环遍历节点数组,清空操作结束。
clear
方法的主要逻辑是遍历哈希表中的节点数组,对每个位置上的节点进行删除操作。在删除过程中,使用同步锁来确保操作的原子性,同时通过计数器记录删除的节点数量,并根据计数更新计数器的值。最后,如果有节点被删除,则根据删除计数更新计数器。通过这样的逻辑,clear
方法实现了清空哈希表的功能。
六、获取元素
get()
方法,用于根据给定的键获取对应的值。
public V get(Object key) {
Node<K,V>[] tab;
Node<K,V> e, p;
int n, eh;
K ek;
int h = spread(key.hashCode());
// 获取哈希表中的节点数组
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
// 如果找到了匹配的节点
if ((eh = e.hash) == h) {
// 如果哈希值相等,比较键是否相等
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
// 如果节点的哈希值为负数,表示当前节点为树节点
else if (eh < 0)
// 在树节点中查找键值对
return (p = e.find(h, key)) != null ? p.val : null;
// 遍历链表查找键值对
while ((e = e.next) != null) {
// 如果哈希值相等,比较键是否相等
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
它通过以下逻辑进行查找:
- 计算给定键的哈希值并扩散(spread)。
- 获取哈希表中的节点数组
tab
。 - 检查节点数组
tab
是否为null,节点数组的长度n
是否大于0,以及索引为(n - 1) & h
的节点e
是否为null。 - 如果存在节点
e
:- 检查节点
e
的哈希值eh
是否与扩散后的哈希值h
相等。- 如果相等,比较键
ek
是否与给定键key
相等。如果相等,则返回节点e
的值。
- 如果相等,比较键
- 如果节点的哈希值
eh
小于0,表示节点是树节点。- 在树节点
e
中查找键值对,并返回找到的值。
- 在树节点
- 遍历节点
e
的链表,查找键值对。- 如果哈希值相等,比较键
ek
是否与给定键key
相等。如果相等,则返回节点e
的值。
- 如果哈希值相等,比较键
- 检查节点
- 如果没有找到匹配的键值对,则返回null。
七、其他方法
size()
方法用于返回ConcurrentHashMap
中键值对的数量。它的实现如下:
public int size() {
long n = sumCount();
return ((n < 0L) ? 0 :
(n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
(int)n);
}
该方法首先调用了sumCount()
方法,该方法用于获取ConcurrentHashMap
当前的计数器值。计数器是通过累加每个节点的计数器值得到的。然后,根据计数器值进行以下处理:
- 如果计数器值
n
小于0,则返回0。这是因为在计数器更新的过程中,可能存在瞬态的负值,需要忽略这些负值。 - 如果计数器值
n
大于Integer.MAX_VALUE
,则返回Integer.MAX_VALUE
。这是因为size()
方法返回一个int
类型的值,因此不能超过Integer.MAX_VALUE
。 - 如果计数器值
n
在0到Integer.MAX_VALUE
之间,则将其转换为int
类型并返回。
isEmpty()
方法的实现,用于判断ConcurrentHashMap
是否为空:
public boolean isEmpty() {
return sumCount() <= 0L; // ignore transient negative values
}
该方法也调用了sumCount()
方法获取计数器值,并进行判断:
- 如果计数器值小于等于0,则返回
true
,表示ConcurrentHashMap
为空。同样,这里忽略了瞬态的负值。 - 如果计数器值大于0,则返回
false
,表示ConcurrentHashMap
不为空。
这两个方法都依赖于sumCount()
方法来获取计数器值,而计数器值反映了当前ConcurrentHashMap
中键值对的数量。通过检查计数器值,可以准确地得知ConcurrentHashMap
的大小和是否为空。
containsKey(Object key)
方法,用于检查ConcurrentHashMap
中是否包含指定的键。
public boolean containsKey(Object key) {
return get(key) != null;
}
该方法通过调用get(key)
方法来获取指定键的值,并判断返回的值是否为null
。如果值不为null
,则说明ConcurrentHashMap
中存在该键,返回true
;否则,返回false
。
在内部实现中,get(key)
方法会根据给定的键执行一系列操作来查找对应的值,如果找到了匹配的键值对,则返回值;如果未找到匹配的键值对,则返回null
。因此,通过判断get(key)
的返回值是否为null
,可以确定ConcurrentHashMap
中是否包含指定的键。
这个方法提供了一种简单的方式来检查ConcurrentHashMap
中是否包含指定的键,而无需显式获取值并进行比较。
getOrDefault(Object key, V defaultValue)
方法用于获取指定键的值,如果键不存在,则返回默认值。其实现如下:
public V getOrDefault(Object key, V defaultValue) {
V v;
return (v = get(key)) == null ? defaultValue : v;
}
get(key)
方法来获取指定键的值,并将结果赋给变量v
。然后,通过三元表达式判断v
是否为null
,如果是,则返回默认值defaultValue
;否则,返回v
。
forEach(BiConsumer<? super K, ? super V> action)
方法用于对ConcurrentHashMap
中的每个键值对执行给定的操作。其实现如下:
public void forEach(BiConsumer<? super K, ? super V> action) {
if (action == null) throw new NullPointerException();
Node<K,V>[] t;
if ((t = table) != null) {
Traverser<K,V> it = new Traverser<K,V>(t, t.length, 0, t.length);
for (Node<K,V> p; (p = it.advance()) != null; ) {
action.accept(p.key, p.val);
}
}
}
该方法首先检查传入的操作action
是否为null
,如果是,则抛出NullPointerException
。然后,获取ConcurrentHashMap
的内部节点数组table
,如果数组不为null
,则创建一个Traverser
对象it
,用于遍历节点数组。接下来,通过循环调用it.advance()
方法来获取节点数组中的每个节点,并将键和值传递给action
的accept()
方法进行处理。
总结
-
内部数据结构:
ConcurrentHashMap
使用了一种分段锁的机制,内部维护了一个由Node
节点组成的数组,每个节点包含一个键-值对。数组的每个元素称为一个"段",每个段都是一个独立的哈希表,有自己的锁。每个段中的节点使用链表或红黑树来解决哈希冲突。 -
并发操作:
ConcurrentHashMap
通过使用分段锁(Segment
)来实现并发控制。每个段具有自己的锁,不同的段可以独立地进行读写操作,以提高并发性能。这种设计在多线程环境下减少了锁的竞争范围。 -
锁的获取和释放:
ConcurrentHashMap
使用sync
数组来存储段的锁。在进行操作之前,首先根据键的哈希值获取对应的段,然后获取该段的锁。对于读操作,多个线程可以同时获取不同段的锁并进行读取。对于写操作,需要获取对应段的锁,并且可能需要进行锁升级,将段内部的链表结构转化为红黑树结构。 -
扩容:
ConcurrentHashMap
采用了分段扩容的策略。在进行扩容时,会对每个段进行扩容,而不是一次性对整个哈希表进行扩容。这种方式减少了扩容时的冲突和数据迁移的规模,提高了扩容的效率。 -
计数器:
ConcurrentHashMap
使用baseCount
字段来维护元素的数量。计数器的更新采用了一种"懒惰"的方式,在需要时才进行精确计算。使用CounterCell
类来处理计数器的并发更新,每个段都有一个CounterCell
数组来存储计数器的增量。
基于JDK 8的ConcurrentHashMap
在并发控制和性能方面进行了优化,采用了分段锁和分段扩容的策略,减少了锁的竞争范围,提高了并发性能。同时,它通过延迟计数器的更新和使用CounterCell
来处理计数器的并发更新,减少了计数器的冲突。这使得ConcurrentHashMap
适用于高并发的读写操作场景。