ConcurrentHashMap内部结构
- JDK1.7:
- 分段锁设计:采用Segment数组结构,每个Segment继承自ReentrantLock,每个Segment内部是一个HashEntry数组。Segment相当于一个小型的HashMap,不同Segment之间的操作可以并发执行。
- HashEntry结构:HashEntry是一个链表结构,用于解决哈希冲突。链表头节点存储在HashEntry数组中,通过hash值定位到对应的数组位置。
- JDK1.8:
- 数组 + 链表 + 红黑树:与HashMap类似,内部由Node数组构成,数组每个位置存储链表或红黑树节点。当链表长度超过阈值(默认为8)且数组长度大于等于64时,链表会转换为红黑树以提高查找效率。
- 摒弃分段锁:采用CAS(Compare - And - Swap)和synchronized关键字结合的方式实现并发控制。在数组初始化和扩容时使用CAS操作,对每个Node的头节点使用synchronized关键字进行同步控制。
并发控制机制
- JDK1.7:
- 分段锁:不同Segment之间的操作可以并发执行,只有对同一个Segment的操作才需要竞争锁。例如,当多个线程同时对不同Segment进行put操作时,不会产生竞争,提高了并发性能。
- JDK1.8:
- CAS操作:在初始化数组和扩容时,通过CAS操作保证操作的原子性,避免多线程同时初始化或扩容导致的数据不一致问题。
- synchronized锁:在对链表或红黑树的头节点进行操作时,使用synchronized关键字锁住头节点,保证同一时间只有一个线程可以对该链表或红黑树进行修改,而其他线程可以对其他链表或红黑树进行操作,提高了并发度。
适合使用ConcurrentHashMap的场景
- 缓存场景:
- 在Java Web应用的缓存模块中,多个线程可能同时读取和写入缓存数据。例如,一个电商应用中,商品详情页的缓存数据可能被多个用户请求读取,同时后台可能有线程更新缓存中的商品信息。使用ConcurrentHashMap可以保证在高并发读写情况下数据的一致性和性能。
- 代码示例:
ConcurrentHashMap<String, Product> productCache = new ConcurrentHashMap<>();
// 读取缓存
Product product = productCache.get(productId);
if (product == null) {
// 从数据库加载
product = loadProductFromDB(productId);
productCache.put(productId, product);
}
// 更新缓存
productCache.put(productId, updatedProduct);
- 计数器场景:
- 在统计网站访问量等场景中,多个线程可能同时对计数器进行增加操作。使用ConcurrentHashMap可以高效地实现并发计数。
- 代码示例:
ConcurrentHashMap<String, AtomicInteger> visitCounter = new ConcurrentHashMap<>();
AtomicInteger count = visitCounter.computeIfAbsent("totalVisits", k -> new AtomicInteger(0));
count.incrementAndGet();
高并发环境下性能提升原理
- 减少锁竞争:
- JDK1.7分段锁:不同Segment之间操作无需竞争锁,大大减少了锁的粒度,多个线程可以并发操作不同Segment,提高了并发度。
- JDK1.8锁细化:对每个Node头节点加锁,而不是对整个Map加锁,进一步减少了锁竞争范围,使得不同链表或红黑树的操作可以并发执行。
- CAS操作:在数组初始化和扩容等操作中,CAS操作避免了传统锁带来的线程阻塞和上下文切换开销,提高了操作的原子性和效率。
- 读写分离:读操作一般不需要加锁(除了在resize期间),写操作通过CAS和synchronized保证数据一致性,这种读写分离的设计在高并发读多写少的场景下能显著提升性能。