用string常量作synchronized锁易引发意外全局阻塞,因字符串常量池导致不同业务线程锁住同一对象;new string()创建新对象使同步失效;intern()虽可统一锁但易引发频繁full gc;推荐使用concurrenthashmap锁容器、guava弱引用interner或分段锁等安全替代方案。
用string常量作synchronized锁,确实可能引发意外的全局阻塞——不是因为锁本身“太重”,而是因为多个本该互不干扰的业务线程,无意中锁住了同一个字符串对象。
字符串常量池导致锁对象意外共享
Java中,字面量字符串(如"user_123"、"order_456")会被自动放入字符串常量池。只要内容相同,无论在哪段代码里出现,它们都指向常量池中同一个对象。
这意味着:
- 模块A中写
synchronized("order_" + orderId) { ... },若orderId是编译期已知值(比如"456"),实际等价于synchronized("order_456") - 模块B中也写了
synchronized("order_456") { ... },哪怕业务完全无关,两个线程也会争抢同一把锁 - 更隐蔽的是:
"order_" + "456"在编译期被优化为"order_456",同样落入常量池
new String()看似隔离,实则失效
有人试图用new String("order_" + userId)来避免共享,但这是无效的:
-
new String(...)每次创建新对象,每个线程拿到的锁对象不同 → 同步彻底失效 - 即使内容一样,
==为false,synchronized无法识别逻辑相等性 - 结果是:本该串行的操作变成并行,数据竞争风险陡增
intern()能统一锁对象,但代价很高
调用str.intern()可强制将堆中字符串映射到常量池,确保等值字符串返回同一引用:
- 优点:
synchronized(userId.intern())能稳定锁定同一用户维度 - 缺点:
intern()在JDK 7+后将字符串存入堆的老年代,大量调用会快速填满老年代,触发频繁Full GC - 尤其在高并发、ID种类多的场景(如订单号、设备号),极易成为GC瓶颈
更安全的替代方案
避免依赖字符串常量池,改用可控、可回收的锁对象:
- 用
ConcurrentHashMap<string object></string>做锁容器:map.computeIfAbsent(key, k -> new Object()),配合弱引用或定时清理 - 引入Guava的
Interner<string></string>:Interner<string> pool = Interners.newWeakInterner()</string>,内部用弱引用管理,避免内存泄漏 - 对固定范围ID(如用户ID为long型),可用
Object[] lockArray = new Object[64]做分段锁,用userId % 64定位槽位 - 优先考虑业务层加锁粒度控制,例如用数据库行锁、Redis分布式锁替代JVM级同步
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











