侧边栏壁纸
博主头像
拾荒的小海螺博主等级

只有想不到的,没有做不到的

  • 累计撰写 228 篇文章
  • 累计创建 19 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

JAVA:Synchronized 能否加锁字符串?

拾荒的小海螺
2025-07-15 / 0 评论 / 0 点赞 / 7 阅读 / 5740 字

1、简述

在 Java 开发中,synchronized 是一种常见的同步机制,用于保证线程安全。但是你有没有思考过这样一个问题:

“synchronized 可以给字符串(String)加锁吗?”

答案是:可以,但你应该非常小心。

本文将深入剖析这个问题,讲清楚背后的机制、风险,并给出实际建议。

0DE56C38-5066-4090-88F0-0E63E45A7202.jpg.jpg


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 是否能加锁字符串(可以,但不推荐);
  • 字符串常量池带来的锁隐患;
  • 如何使用 ObjectConcurrentHashMap 构建安全锁;
  • 如何用 Guava 的 Interner 提供高效、可控的锁机制;
  • 方法参数中加锁字符串的风险及解决方案。

写高质量的并发代码,关键是理解锁的语义、作用域和生命周期。希望这篇文章能帮你在并发之路上走得更稳更远。

0

评论区