1.介绍ConcurrentHashMap
ConcurrentHashMap
是 Java 中一个线程安全且高效的哈希表实现,它位于 java.util.concurrent
包下,在多线程环境下可以替代 HashTable
和同步包装器 Collections.synchronizedMap
。以下将详细介绍 ConcurrentHashMap
的底层原理和常用使用方法。
2.底层原理
1. Java 7 版本
在 Java 7 中,ConcurrentHashMap
采用分段锁(Segment)机制来实现线程安全。
- 数据结构:
ConcurrentHashMap
内部包含一个Segment
数组,每个Segment
类似于一个小的HashMap
,它继承自ReentrantLock
。每个Segment
维护着一个HashEntry
数组,HashEntry
是一个链表节点,用于存储键值对。 - 分段锁机制:不同的
Segment
可以被不同的线程同时访问,因此在多线程环境下,只要访问的是不同的Segment
,就不会产生锁竞争,从而提高了并发性能。只有当多个线程访问同一个Segment
时,才需要竞争该Segment
上的锁。
2. Java 8 版本
Java 8 对 ConcurrentHashMap
进行了重大改进,摒弃了分段锁机制,采用 CAS(Compare-And-Swap)和 synchronized
来保证并发安全。
- 数据结构:
ConcurrentHashMap
采用数组 + 链表 + 红黑树的结构。当链表长度超过一定阈值(默认为 8)且数组长度大于 64 时,链表会转换为红黑树,以提高查找效率。 - CAS 和
synchronized
:- CAS:在插入或更新元素时,首先使用 CAS 操作尝试直接修改数组中的节点,如果操作成功,则表示插入或更新成功;如果失败,则说明有其他线程在同时操作,此时需要使用
synchronized
进行同步。 synchronized
:当需要对某个节点进行修改时,会使用synchronized
对该节点进行加锁,保证同一时刻只有一个线程可以对该节点进行操作。
- CAS:在插入或更新元素时,首先使用 CAS 操作尝试直接修改数组中的节点,如果操作成功,则表示插入或更新成功;如果失败,则说明有其他线程在同时操作,此时需要使用
3.常用使用方法
1. 初始化 ConcurrentHashMap
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentHashMapExample {
public static void main(String[] args) {
// 初始化一个空的 ConcurrentHashMap
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
}
}
2. 添加元素
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentHashMapExample {
public static void main(String[] args) {
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// 添加单个元素
map.put("apple", 1);
// 如果指定的键不存在,则添加该键值对
map.putIfAbsent("banana", 2);
}
}
3. 获取元素
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentHashMapExample {
public static void main(String[] args) {
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("apple", 1);
// 根据键获取值
Integer value = map.get("apple");
System.out.println(value); // 输出: 1
}
}
4. 删除元素
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentHashMapExample {
public static void main(String[] args) {
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("apple", 1);
// 根据键删除元素
Integer removedValue = map.remove("apple");
System.out.println(removedValue); // 输出: 1
}
}
5. 遍历元素
import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;
public class ConcurrentHashMapExample {
public static void main(String[] args) {
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("apple", 1);
map.put("banana", 2);
// 使用 for-each 循环遍历键值对
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
}
}
6. 统计元素数量
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentHashMapExample {
public static void main(String[] args) {
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("apple", 1);
map.put("banana", 2);
// 获取元素数量
int size = map.size();
System.out.println(size); // 输出: 2
}
}
4.ConcurrenntHashMap在java8中具体做了哪些优化
在 Java 8 中,ConcurrentHashMap
相较于 Java 7 版本有了重大的优化,下面从数据结构、锁机制、并发操作等方面详细介绍这些优化点。
1. 数据结构优化
- 摒弃分段锁,采用数组 + 链表 + 红黑树
- Java 7:使用分段锁(
Segment
)机制,ConcurrentHashMap
内部是一个Segment
数组,每个Segment
又包含一个HashEntry
数组,本质上相当于多个小的HashMap
。 - Java 8:采用数组 + 链表 + 红黑树的结构。数组用于存储节点,当发生哈希冲突时,相同哈希值的节点会以链表的形式存储在数组的对应位置。当链表长度超过一定阈值(默认为 8)且数组长度大于 64 时,链表会转换为红黑树;当红黑树节点数量小于等于 6 时,红黑树会转换回链表。这种结构在处理哈希冲突时,尤其是在链表较长的情况下,能显著提高查找、插入和删除操作的效率,因为红黑树的时间复杂度为 O(log n),而链表为 O(n)。
- Java 7:使用分段锁(
2. 锁机制优化
- 使用 CAS 和
synchronized
替代分段锁- Java 7:使用分段锁,不同的
Segment
可以被不同的线程同时访问,只有访问同一个Segment
时才会产生锁竞争。但这种方式在某些场景下可能会导致锁的粒度较大,影响并发性能。 - Java 8:采用 CAS(Compare-And-Swap)和
synchronized
来保证并发安全。- CAS:在插入或更新元素时,首先使用 CAS 操作尝试直接修改数组中的节点。CAS 是一种无锁算法,通过比较内存中的值和预期值是否相等来决定是否更新,操作是原子性的。如果操作成功,则表示插入或更新成功;如果失败,则说明有其他线程在同时操作,此时需要使用
synchronized
进行同步。 synchronized
:当需要对某个节点进行修改时,会使用synchronized
对该节点进行加锁,保证同一时刻只有一个线程可以对该节点进行操作。synchronized
在 Java 8 中进行了大量优化,性能有了显著提升,而且锁的粒度更小,只对单个节点加锁,相比 Java 7 的分段锁,并发性能得到了进一步提高。
- CAS:在插入或更新元素时,首先使用 CAS 操作尝试直接修改数组中的节点。CAS 是一种无锁算法,通过比较内存中的值和预期值是否相等来决定是否更新,操作是原子性的。如果操作成功,则表示插入或更新成功;如果失败,则说明有其他线程在同时操作,此时需要使用
- Java 7:使用分段锁,不同的
3. 并发操作优化
- 支持并发的
compute
、computeIfAbsent
和computeIfPresent
等方法compute
方法:允许根据键的当前值(如果存在)和给定的计算函数来计算并更新值。这个方法在多线程环境下可以安全地执行更新操作,避免了先检查后更新带来的并发问题。computeIfAbsent
方法:如果指定的键不存在,则使用给定的函数计算一个值,并将其插入到ConcurrentHashMap
中。这个方法可以避免在多线程环境下重复计算相同键的值。computeIfPresent
方法:如果指定的键存在,则使用给定的函数计算一个新值,并更新该键对应的值。
4. 遍历操作优化
- 弱一致性迭代器:Java 8 的
ConcurrentHashMap
提供了弱一致性的迭代器,在迭代过程中,即使其他线程对ConcurrentHashMap
进行了修改,迭代器也能继续工作,不会抛出ConcurrentModificationException
。这是因为迭代器在创建时会记录当前ConcurrentHashMap
的快照,在迭代过程中会尽量反映出创建时的状态,同时也会尝试反映后续的修改,但不保证完全一致。这种弱一致性的设计在多线程环境下提供了更好的并发性能。
以下是一个使用 computeIfAbsent
方法的示例代码:
java
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentHashMapComputeIfAbsentExample {
public static void main(String[] args) {
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// 如果键 "apple" 不存在,则计算值并插入
Integer value = map.computeIfAbsent("apple", k -> 1);
System.out.println(value); // 输出: 1
}
}
综上所述,Java 8 的 ConcurrentHashMap
通过数据结构、锁机制、并发操作和遍历操作等方面的优化,提高了并发性能和使用的便利性。