一、介绍
可以支持高并发的读写操作,并且具有很好的扩展性。它是在 Java 1.5 中引入的,并且在 Java 1.8 中进行了优化。ConcurrentHashMap 底层采用分段锁技术来实现线程安全,其内部结构包含多个 Segment,每个 Segment 本质上就是一个线程安全的哈希表。
二、特性
1. 线程安全
put、get、remove 等操作都是线程安全的,不需要额外的同步机制,因此可以在多线程环境下安全地使用。
2. 高效性
通过使用分段锁技术来实现线程安全,每个 Segment 都拥有一把锁,不同的线程可以同时访问不同的 Segment,从而提高并发性。
4. 支持高并发读写操作
支持多个线程同时进行读操作,而且不会阻塞写操作,因此可以在高并发环境下保持较好的性能。
5. 支持更多的操作
replace、putIfAbsent 等,方便开发人员实现更多的业务需求。
6. ConcurrentHashMap 的性能优化
Java 8 中对其进行了一些优化。其中一个重要的优化是利用 CAS 操作替换了之前的 Synchronized 关键字,从而减少了锁的争用,提高了并发度。
三、原理
采用分段锁技术来实现线程安全,其内部结构包含多个 Segment,每个 Segment 本质上就是一个线程安全的哈希表。每个 Segment 都拥有一把锁,不同的线程可以同时访问不同的 Segment,从而提高并发性。在读操作时,可以同时支持多个线程进行读操作,而在写操作时,只需要锁住对应的 Segment,不会对其他的 Segment 进行影响。
哈希表采用数组 + 链表 + 红黑树的数据结构实现,其中数组是用来存储哈希桶的,链表和红黑树则是用来解决哈希冲突的。当链表中的元素达到一定的数量时,链表会被转换成红黑树,以提高查找和删除操作的性能。
四、使用场景
1. 高并发环境
适合在高并发环境下使用,特别是读写操作频繁的场景。在这种场景下,使用普通的HashMap 可能会出现线程安全问题,使用Hashtable的话效率相对较低。
2. 大规模数据存储
并发性能非常好,并且支持动态调整 Segment 的数量,因此可以很好地支持大规模数据的存储和处理。
3. 缓存
非常适合作为缓存的存储结构,因为它支持高并发读写操作,并且具有很好的扩展性。在缓存中,读操作比写操作要频繁得多,因此可以在保证线程安全的同时提高缓存的性能。
五、注意事项
1. ConcurrentHashMap 的迭代器是弱一致性的
可能会出现一些数据的不一致性问题。虽然 ConcurrentHashMap 提供了多种遍历方式,但是无论是使用迭代器、forEach 还是 Spliterator,都需要注意它们是弱一致性的,可能会看到过时的数据。
2. ConcurrentHashMap 的 putIfAbsent() 方法
putIfAbsent(K key, V value) 方法,可以将一个 key-value 对添加到 ConcurrentHashMap 中,但是只有在该 key 还不存在的情况下才会添加成功。这个方法常常用于缓存中的添加操作,可以避免重复添加相同的数据。
3. ConcurrentHashMap 的 size() 方法
并不总是返回准确的大小,因为在多线程环境下,可能会存在一些数据的不一致性。虽然 ConcurrentHashMap 提供了一些获取近似大小的方法,但是需要注意它们的精度可能会受到一些限制。
4. 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就不会出现上述情况。