1、简述
在 java 开发中,synchronized
是一种常见的同步机制,用于保证线程安全。但是你有没有思考过这样一个问题:
“synchronized 可以给字符串(string)加锁吗?”
答案是:可以,但你应该非常小心。
本文将深入剖析这个问题,讲清楚背后的机制、风险,并给出实际建议。
2、synchronized 本质上加的是什么锁?
synchronized
实际上加的是对象锁,也叫监视器锁(monitor lock)。也就是说:
synchronized (obj) { // 临界区 }
这段代码表示:只有获取到 obj
这个对象的监视器锁的线程才能进入临界区。
因此,只要是一个对象,包括字符串实例,理论上都可以被用作加锁对象。
3、加锁字符串——看似可行,实则隐患巨大
来看一个例子:
public class stringlockexample { public void dosomething(string lock) { synchronized (lock) { system.out.println(thread.currentthread().getname() + " 获得了锁:" + lock); try { thread.sleep(1000); } catch (interruptedexception ignored) {} } } }
启动多个线程调用:
stringlockexample example = new stringlockexample(); runnable task1 = () -> example.dosomething("lock"); runnable task2 = () -> example.dosomething("lock"); new thread(task1).start(); new thread(task2).start();
结果是,两个线程会串行执行,因为加的是同一个字符串 "lock"
的锁。
但问题来了:
字符串是不可变对象,且 jvm 对字符串常量具有“字符串池”优化机制(string interning)!
也就是说:
string a = "lock"; string b = "lock"; system.out.println(a == b); // true
两个字符串变量实际上引用的是同一个对象。
因此,在你以为传进来的是不同的字符串时,可能实际上加的是同一把锁,或者反过来——你以为加的是同一把锁,其实不是!
4、字符串加锁的两个典型陷阱
4.1 锁粒度无法控制
如果你的锁是这样定义的:
synchronized ("user_" + userid)
你以为这是“每个用户一个锁”,但实际上由于字符串拼接会创建新对象,每次拼接都是一个新对象,锁根本不会生效。
除非你手动 .intern()
:
synchronized (("user_" + userid).intern())
这又引入了新的问题:intern 的对象存储在字符串常量池中,频繁使用可能会增加内存压力,甚至引发性能问题。
4.2 外部可控锁对象
如果你用外部传入的字符串作为锁对象,那你根本无法控制到底加的是什么锁。恶意或不规范调用者可能传入一个常量字符串、空字符串、甚至 null,导致同步行为混乱或抛出异常。
5、安全的替代方案
✅ 使用自定义锁对象
最推荐的方式是自己定义一套锁策略,例如使用 concurrenthashmap
管理锁对象:
private final concurrenthashmap<string, object> lockmap = new concurrenthashmap<>(); public void dosomething(string key) { object lock = lockmap.computeifabsent(key, k -> new object()); synchronized (lock) { // 临界区 } }
这种方式可以保证每个业务 key 对应一个明确的锁对象,而且不会误用常量字符串,锁粒度清晰可控。
6、使用 google guava 的 interner 实现更安全的字符串锁
interner
是 google guava 提供的一个实用工具类,用于实现“字符串实例的唯一化”。它的作用类似于 string.intern()
,但更灵活、可控,不依赖字符串常量池,避免了 jvm 层级的内存污染和性能隐患。
引入依赖:
<!-- maven --> <dependency> <groupid>com.google.guava</groupid> <artifactid>guava</artifactid> <version>32.1.1-jre</version> </dependency>
使用示例:
import com.google.common.collect.interner; import com.google.common.collect.interners; public class guavainternerlock { private static final interner<string> interner = interners.newweakinterner(); public void dowork(string key) { string internedkey = interner.intern(key); synchronized (internedkey) { // 同样 key 的线程会同步执行 system.out.println("processing key: " + key); } } }
guava interner 的优势
- 不污染 jvm 的字符串常量池(不像
string.intern()
)。 - 可以选择 weak 或 strong 引用,避免内存泄漏。
- 适合在缓存、去重、分布式任务分片等场景中锁定“逻辑键”。
7、总结
并发编程中,锁不是万能的,滥用锁更是灾难。本文完整地分析了:
synchronized
是否能加锁字符串(可以,但不推荐);- 字符串常量池带来的锁隐患;
- 如何使用
object
、concurrenthashmap
构建安全锁; - 如何用 guava 的
interner
提供高效、可控的锁机制; - 方法参数中加锁字符串的风险及解决方案。
写高质量的并发代码,关键是理解锁的语义、作用域和生命周期。希望这篇文章能帮你在并发之路上走得更稳更远。
到此这篇关于java中synchronized能否加锁字符串的文章就介绍到这了,更多相关java synchronized加锁字符串内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论