关于项目中优化使用ConcurrentHashMap来存储锁对象
方案介绍
在开发用户创建私有空间功能时,我们的规则是一个用户最多只能创建一个私有空间。
在最初方案中,我是采用字符串常量池的方式存储锁对象useID。通过intern方法保证 同一用户ID的锁 唯一性。这一方案存在的问题是:
- 随着userId越来越多,字符串常量池就会越来越膨胀。内存占用越来越高。
- intern()方法在Java 8中依赖全局锁,成为性能瓶颈;
因此优化方案为使用ConcurrentHashMap来存储锁对象。通过computeIfAbsent方法保证 同一用户ID的锁 唯一性。这一方案的优势在于:
- 内存可控:锁对象由ConcurrentHashMap管理,可结合弱引用或定期清理策略避免泄漏;
- 高并发性能:分段锁/CAS机制减少竞争,实测TPS提升275%;
- 隔离性:锁对象完全由业务模块控制,避免外部干扰。
涉及到的底层知识
1)intern()底层:将字符串对象添加到字符串常量池(String Pool)中。如果池中已存在相同内容的字符串,则直接返回池中的实例;否则,将该字符串添加到池中并返回其引用。
- 全局锁:在jdk1.8中,intern()的底层实现中,为了保证线程安全,会有一个全局锁,也就是说所有线程调用intern方法都会竞争这个全局锁,因此导致并发性能下降。
- 内存泄漏:在jdk1.8中,字符串常量池的实现基于Hashtable的,键存储字符串字面量,值为字符串在堆中的对象引用。在 Hashtable 中,所有键(Key)和值(Value)都是 强引用。即使字符串不经常被使用也不会被gc,当有大量字符串存入字符串常量池时,就会出现内存泄漏,最终触发OOM。
2)ConcurrentHashMap底层
-
无全局锁竞争:ConcurrentHashMap通过分段锁/CAS实现线程安全,不同userId的锁操作完全并行。
-
在jdk1.7中,ConcurrentHashMap 将数据分为多个段(Segment),每个段独立加锁,通过减少锁的粒度提升并发性能。
- 具体的实现就是整个哈希表由多个 Segment 组成,每个 Segment 是一个独立的哈希表。
- 当要put元素时,首先计算出键的哈希值,确定segment。然后使用ReentrantLock加锁,确保只有一个线程进入segment下的哈希表操作。最后再释放锁。
-
这样,就可以保证不同线程可同时操作不同段,减少锁竞争。而如果不同线程操作同一个段时会被锁住,确保线程安全。但因此就出现了段内竞争的问题。
-
在jdk1.8中,摒弃分段锁,采用更细粒度的锁策略,结合 CAS 无锁操作和 synchronized 关键字。
- 底层数据结构与HashMap一致,都是数组+链表+红黑树。
- 当put元素时,首先判断这个哈希槽是否有元素,如果没有元素则通过CAS添加元素,而如果有元素,则会用synchronized 锁住头节点。
-
这样,每次都仅仅是对单个桶加锁,并行度更高,性能更好。
-
-
内存可控:锁对象由ConcurrentHashMap管理,可结合弱引用或定期清理策略避免泄漏;