目录
一、介绍
ConcurrentHashMap 是 JDK 提供的线程安全的哈希表实现,它可以支持高并发的读写操作,并且具有很好的扩展性。它是在 Java 1.5 中引入的,并且在 Java 1.8 中进行了优化。ConcurrentHashMap 底层采用分段锁技术来实现线程安全,其内部结构包含多个 Segment,每个 Segment 本质上就是一个线程安全的哈希表。
二、特性
1. 线程安全
ConcurrentHashMap 的 put、get、remove 等操作都是线程安全的,不需要额外的同步机制,因此可以在多线程环境下安全地使用。
2. 高效性
ConcurrentHashMap 在并发环境下可以提供较高的吞吐量,它通过使用分段锁技术来实现线程安全,每个 Segment 都拥有一把锁,不同的线程可以同时访问不同的 Segment,从而提高并发性。
3. 支持高并发读写操作
ConcurrentHashMap 支持多个线程同时进行读操作,而且不会阻塞写操作,因此可以在高并发环境下保持较好的性能。
4. 支持更多的操作
ConcurrentHashMap 还提供了一些其他的操作,如 replace、putIfAbsent 等,方便开发人员实现更多的业务需求。
5. ConcurrentHashMap 的性能优化
为了进一步提高 ConcurrentHashMap 的性能,Java 8 中对其进行了一些优化。其中一个重要的优化是利用 CAS 操作替换了之前的 Synchronized 关键字,从而减少了锁的争用,提高了并发度。
三、原理
ConcurrentHashMap 采用分段锁技术来实现线程安全,其内部结构包含多个 Segment,每个 Segment 本质上就是一个线程安全的哈希表。每个 Segment 都拥有一把锁,不同的线程可以同时访问不同的 Segment,从而提高并发性。在读操作时,可以同时支持多个线程进行读操作,而在写操作时,只需要锁住对应的 Segment,不会对其他的 Segment 进行影响。
ConcurrentHashMap 中的哈希表采用数组 + 链表 + 红黑树的数据结构实现,其中数组是用来存储哈希桶的,链表和红黑树则是用来解决哈希冲突的。当链表中的元素达到一定的数量时,链表会被转换成红黑树,以提高查找和删除操作的性能。
四、使用场景
1. 高并发环境
ConcurrentHashMap最适合在高并发环境下使用,特别是读写操作频繁的场景。在这种场景下,使用普通的HashMap 可能会出现线程安全问题,使用Hashtable的话效率相对较低。
2. 大规模数据存储
由于 ConcurrentHashMap 内部采用分段锁技术来实现线程安全,因此它的并发性能非常好,并且支持动态调整 Segment 的数量,因此可以很好地支持大规模数据的存储和处理。
3. 缓存
ConcurrentHashMap 也非常适合作为缓存的存储结构,因为它支持高并发读写操作,并且具有很好的扩展性。在缓存中,读操作比写操作要频繁得多,因此可以在保证线程安全的同时提高缓存的性能。
五、注意事项
1. ConcurrentHashMap 的迭代器是弱一致性的
由于 ConcurrentHashMap 是一个并发容器,因此在遍历容器时,可能会出现一些数据的不一致性问题。虽然 ConcurrentHashMap 提供了多种遍历方式,但是无论是使用迭代器、forEach 还是 Spliterator,都需要注意它们是弱一致性的,可能会看到过时的数据。
2. ConcurrentHashMap 的 putIfAbsent() 方法
ConcurrentHashMap 中提供了一个 putIfAbsent(K key, V value) 方法,可以将一个 key-value 对添加到 ConcurrentHashMap 中,但是只有在该 key 还不存在的情况下才会添加成功。这个方法常常用于缓存中的添加操作,可以避免重复添加相同的数据。
3. ConcurrentHashMap 的 size() 方法
ConcurrentHashMap 中的 size() 方法并不总是返回准确的大小,因为在多线程环境下,可能会存在一些数据的不一致性。虽然 ConcurrentHashMap 提供了一些获取近似大小的方法,但是需要注意它们的精度可能会受到一些限制。
4. ConcurrentHashMap 的初始化容量
在创建 ConcurrentHashMap 对象时,需要指定其初始化容量和负载因子。为了提高性能,建议将初始化容量设置为预期存储的 key-value 对数量的两倍左右。如果初始化容量设置得太小,可能会导致扩容过程比较频繁,影响性能。
六、实际应用
1. 案例一
(1) 场景
使用ConcurrentHashMap的一些方法的案例。
(2) 代码
import java.util.concurrent.ConcurrentHashMap;
/**
* ConcurrentHashMapCase1
* 简单使用ConcurrentHashMap的一些方法
*
* @author wxy
* @since 2023-04-26
*/
public class ConcurrentHashMapCase1 {
public static void main(String[] args) {
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("one", 1);
map.put("two", 2);
map.put("three", 3);
// 输出 1
System.out.println(map.get("one"));
map.putIfAbsent("four", 4);
map.putIfAbsent("four", 5);
map.putIfAbsent("four", 6);
// 输出 4
System.out.println(map.get("four"));
map.replace("two", 22);
// 输出 22
System.out.println(map.get("two"));
map.remove("three");
// 输出 null
System.out.println(map.get("three"));
}
}
在这个示例代码中,我们创建了一个 ConcurrentHashMap 对象,并向其中添加了三个 key-value 对。然后我们使用 get() 、putIfAbsent()、replace() 、remove() 方法进行演示。
2. 案例二
(1) 场景
ConcurrentHashMap和HashMap在多线程情况下, 缓存数据准确程度。
(2) 代码
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* ConcurrentHashMapCase2: ConcurrentHashMap相对于HashMap有什么根本区别呢?
* CopyOnWriteArrayList: 线程安全。
* ArrayList: 线程不安全。
* ConcurrentHashMap和HashMap在多线程情况下, 缓存数据准确程度。
*
* @author wxy
* @since 2023-04-26
*/
public class ConcurrentHashMapCase2 {
public static void main(String[] args) throws InterruptedException {
Map<String, String> concurrentHashMap = new ConcurrentHashMap<>(500);
Map<String, String> hashMap = new HashMap<>(500);
// 创建两个线程并启动它们
Thread thread1 = new Thread(() -> {
for (int index1 = 0; index1 < 1000; index1++) {
concurrentHashMap.put(Integer.toString(index1), Integer.toBinaryString(index1));
hashMap.put(Integer.toString(index1), Integer.toBinaryString(index1));
}
});
Thread thread2 = new Thread(() -> {
for (int index2 = 1000; index2 < 2000; index2++) {
concurrentHashMap.put(Integer.toString(index2), Integer.toBinaryString(index2));
hashMap.put(Integer.toString(index2), Integer.toBinaryString(index2));
}
});
thread1.start();
thread2.start();
// 等待两个线程结束
thread1.join();
thread2.join();
System.out.println("ConcurrentHashMap size: " + concurrentHashMap.size());
System.out.println("HashMap size: " + hashMap.size());
}
}
运行结果如下:
为什么在上述代码中,HashMap没有相同的键值对,却导致一部分键值被覆盖了呢?实际上,即使没有相同的键值对,也可能会出现这种情况。
这是因为HashMap不是线程安全的,它的内部结构由一个数组和一个单向链表组成。在多线程环境下,如果两个线程同时调用put方法,可能会出现以下情况:
1. 线程1正在往数组位置i处添加一个键值对,但还没有将链表上的其他元素复制到新的链表中。
2. 线程2也要往数组位置i处添加一个键值对,发现此时链表上还只有旧的元素,于是也将它们复制到新的链表中。
3. 线程1完成了它的操作,将新的键值对插入到新的链表的头部。
4. 线程2完成了它的操作,将新的键值对插入到新的链表的头部。
由于HashMap的put方法并没有同步,所以这种情况下会出现并发修改异常。其中一个键值对会被覆盖,最终导致HashMap大小不到预期值。如果使用ConcurrentHashMap就不会出现上述情况。